|
|
@@ -0,0 +1,168 @@
|
|
|
+using Mcp;
|
|
|
+using Gee;
|
|
|
+
|
|
|
+public class SlopdocsProvider : Mcp.Resources.BaseProvider {
|
|
|
+ private string slopdocs_dir;
|
|
|
+ private HashMap<string, SlopdocInfo> slopdocs_cache;
|
|
|
+
|
|
|
+ public class SlopdocInfo : GLib.Object {
|
|
|
+ public string file_path { get; set; }
|
|
|
+ public string summary { get; set; }
|
|
|
+ public string title { get; set; }
|
|
|
+ public DateTime last_modified { get; set; }
|
|
|
+
|
|
|
+ public SlopdocInfo (string file_path, string summary, string title, DateTime last_modified) {
|
|
|
+ this.file_path = file_path;
|
|
|
+ this.summary = summary;
|
|
|
+ this.title = title;
|
|
|
+ this.last_modified = last_modified;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ public SlopdocsProvider (string slopdocs_directory) {
|
|
|
+ this.slopdocs_dir = slopdocs_directory;
|
|
|
+ this.slopdocs_cache = new HashMap<string, SlopdocInfo> ();
|
|
|
+ refresh ();
|
|
|
+ }
|
|
|
+
|
|
|
+ public void refresh () {
|
|
|
+ slopdocs_cache.clear ();
|
|
|
+
|
|
|
+ try {
|
|
|
+ var directory = File.new_for_path (slopdocs_dir);
|
|
|
+ if (!directory.query_exists ()) {
|
|
|
+ warning ("Slopdocs directory does not exist: %s", slopdocs_dir);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ var enumerator = directory.enumerate_children (
|
|
|
+ "standard::name,standard::type,standard::content-type,time::modified",
|
|
|
+ FileQueryInfoFlags.NONE
|
|
|
+ );
|
|
|
+
|
|
|
+ FileInfo file_info;
|
|
|
+ while ((file_info = enumerator.next_file ()) != null) {
|
|
|
+ if (file_info.get_file_type () == FileType.REGULAR &&
|
|
|
+ file_info.get_name ().has_suffix (".md")) {
|
|
|
+
|
|
|
+ var file_path = Path.build_filename (slopdocs_dir, file_info.get_name ());
|
|
|
+ var modification_time = file_info.get_modification_date_time ();
|
|
|
+ var slopdoc_info = parse_slopdoc (file_path, modification_time.to_unix ());
|
|
|
+ var resource_name = get_resource_name_from_filename (file_info.get_name ());
|
|
|
+ slopdocs_cache[resource_name] = slopdoc_info;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } catch (Error e) {
|
|
|
+ warning ("Failed to scan slopdocs directory: %s", e.message);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private string get_resource_name_from_filename (string filename) {
|
|
|
+ // Convert filename to resource URI format
|
|
|
+ // e.g., "ethics.slopdocs.md" -> "slopdocs://ethics.slopdocs.md"
|
|
|
+ return "slopdocs://%s".printf (filename);
|
|
|
+ }
|
|
|
+
|
|
|
+ private SlopdocInfo parse_slopdoc (string file_path, int64 modified_time) {
|
|
|
+ SlopdocInfo info = new SlopdocInfo ("", "", "", new DateTime.now ());
|
|
|
+ info.file_path = file_path;
|
|
|
+ info.last_modified = new DateTime.from_unix_utc (modified_time);
|
|
|
+
|
|
|
+ try {
|
|
|
+ string content;
|
|
|
+ FileUtils.get_contents (file_path, out content);
|
|
|
+
|
|
|
+ // Extract title from first H1 heading
|
|
|
+ var lines = content.split ("\n");
|
|
|
+ foreach (var line in lines) {
|
|
|
+ if (line.has_prefix ("# ")) {
|
|
|
+ info.title = line.substring (2).strip ();
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // If no title found, use filename without extension
|
|
|
+ if (info.title == null || info.title == "") {
|
|
|
+ info.title = Path.get_basename (file_path).replace (".md", "");
|
|
|
+ }
|
|
|
+
|
|
|
+ // Extract summary from the end of the document
|
|
|
+ // Summary is the text after the final "---" marker
|
|
|
+ var parts = content.split ("---");
|
|
|
+ if (parts.length > 1) {
|
|
|
+ var summary_section = parts[parts.length - 1].strip ();
|
|
|
+ if (summary_section != "") {
|
|
|
+ info.summary = summary_section;
|
|
|
+ } else {
|
|
|
+ info.summary = "No summary available";
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ info.summary = "No summary available";
|
|
|
+ }
|
|
|
+
|
|
|
+ } catch (Error e) {
|
|
|
+ warning ("Failed to parse slopdoc %s: %s", file_path, e.message);
|
|
|
+ info.title = Path.get_basename (file_path).replace (".md", "");
|
|
|
+ info.summary = "Failed to parse document";
|
|
|
+ }
|
|
|
+
|
|
|
+ return info;
|
|
|
+ }
|
|
|
+
|
|
|
+ public override async Gee.ArrayList<Mcp.Resources.Types.Resource> list_resources (string? cursor) throws Error {
|
|
|
+ var resources = new Gee.ArrayList<Mcp.Resources.Types.Resource> ();
|
|
|
+
|
|
|
+ foreach (var entry in slopdocs_cache.entries) {
|
|
|
+ var resource = new Mcp.Resources.Types.Resource (
|
|
|
+ entry.key,
|
|
|
+ entry.value.title
|
|
|
+ );
|
|
|
+ resource.description = entry.value.summary;
|
|
|
+ resource.mime_type = "text/markdown";
|
|
|
+
|
|
|
+ resources.add (resource);
|
|
|
+ }
|
|
|
+
|
|
|
+ return resources;
|
|
|
+ }
|
|
|
+
|
|
|
+ public override async Gee.ArrayList<Mcp.Types.Common.ResourceContents> read_resource (string uri) throws Error {
|
|
|
+ if (!uri.has_prefix ("slopdocs://")) {
|
|
|
+ throw new Mcp.Core.McpError.INVALID_REQUEST ("Invalid URI scheme");
|
|
|
+ }
|
|
|
+
|
|
|
+ // The URI should be the full resource name including filename
|
|
|
+ // e.g., "slopdocs://structure.slopdocs.md"
|
|
|
+ var resource_name = uri; // Use the full URI as the key
|
|
|
+
|
|
|
+ if (!slopdocs_cache.has_key (resource_name)) {
|
|
|
+ throw new Mcp.Core.McpError.RESOURCE_NOT_FOUND ("Resource not found: %s".printf (resource_name));
|
|
|
+ }
|
|
|
+
|
|
|
+ var slopdoc_info = slopdocs_cache[resource_name];
|
|
|
+
|
|
|
+ try {
|
|
|
+ string content;
|
|
|
+ FileUtils.get_contents (slopdoc_info.file_path, out content);
|
|
|
+
|
|
|
+ // Return as text content wrapped in ArrayList
|
|
|
+ var result = new Gee.ArrayList<Mcp.Types.Common.ResourceContents> ();
|
|
|
+ result.add (new Mcp.Types.Common.TextResourceContents (uri, content));
|
|
|
+
|
|
|
+ return result;
|
|
|
+
|
|
|
+ } catch (Error e) {
|
|
|
+ throw new Mcp.Core.McpError.INTERNAL_ERROR ("Failed to read file: %s".printf (e.message));
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ public override async void subscribe (string uri) throws Error {
|
|
|
+ // For now, we don't support subscriptions
|
|
|
+ throw new Mcp.Core.McpError.INVALID_REQUEST ("Subscriptions not supported");
|
|
|
+ }
|
|
|
+
|
|
|
+ public override async void unsubscribe (string uri) throws Error {
|
|
|
+ // For now, we don't support subscriptions
|
|
|
+ throw new Mcp.Core.McpError.INVALID_REQUEST ("Subscriptions not supported");
|
|
|
+ }
|
|
|
+}
|