UNPKG

@inkdropapp/mcp-server

Version:

Inkdrop Model Context Protocol Server

332 lines (331 loc) 12.2 kB
"use strict"; /** * Example: MCP Proxy for Inkdrop's local HTTP server. * * Requirements: * 1) Inkdrop's local server is running at config.baseUrl (e.g. http://localhost:19840). * 2) A Basic auth username/password is set in Inkdrop. * 3) This script can be run with `npx ts-node inkdrop-mcp-proxy.ts`. * 4) Then, an MCP client (e.g. MCP Inspector, or Claude) can connect to: * GET http://localhost:3002/sse (SSE endpoint) * POST http://localhost:3002/messages (MCP messages endpoint) */ Object.defineProperty(exports, "__esModule", { value: true }); const url_1 = require("url"); const mcp_js_1 = require("@modelcontextprotocol/sdk/server/mcp.js"); const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js"); const zod_1 = require("zod"); /** Configuration for talking to Inkdrop’s local HTTP server */ const config = { baseUrl: "http://localhost:19841", // or https://localhost:19840 username: "foo@bar.com", password: "bar", /** If Inkdrop uses self-signed cert over HTTPS, you may need to allow that. */ }; /** Build the 'Authorization: Basic ...' header */ function getAuthHeaders() { const token = Buffer.from(`${config.username}:${config.password}`).toString("base64"); return { Authorization: `Basic ${token}` }; } /** Helper to attach query parameters to a given path */ function buildUrl(path, qs = {}) { const url = new url_1.URL(path, config.baseUrl); for (const [k, v] of Object.entries(qs)) { if (v !== undefined && v !== null) url.searchParams.set(k, String(v)); } return url.toString(); } /** Helper to do a GET request and return JSON or throw an error. */ async function fetchJSON(path, qs = {}) { const url = buildUrl(path, qs); const resp = await fetch(url, { headers: getAuthHeaders() }); if (!resp.ok) { throw new Error(`Fetch error [${resp.status}] ${resp.statusText}`); } return await resp.json(); } /** Helper to do a POST request with JSON body */ async function postJSON(path, body) { const url = buildUrl(path); const resp = await fetch(url, { method: "POST", headers: { ...getAuthHeaders(), "Content-Type": "application/json" }, body: JSON.stringify(body) }); if (!resp.ok) { throw new Error(`Fetch error [${resp.status}] ${resp.statusText}`); } return await resp.json(); } /** Helper to do a DELETE request */ async function deleteJSON(path) { const url = buildUrl(path); const resp = await fetch(url, { method: "DELETE", headers: getAuthHeaders() }); if (!resp.ok) { throw new Error(`Fetch error [${resp.status}] ${resp.statusText}`); } return await resp.json(); } /** Create the MCP Server that proxies to Inkdrop’s local API. */ function createInkdropProxyServer() { const server = new mcp_js_1.McpServer({ name: "Inkdrop MCP Proxy", version: "1.0.0" }); // // 1) Root meta info (like GET "/") // server.resource("root", "inkdrop://root", async (uri) => { const body = await fetchJSON("/"); return { contents: [ { uri: uri.href, text: JSON.stringify(body, null, 2) } ] }; }); // // 2) Notes // // 2a) List all notes (proxy GET /notes, with query params) server.resource("notes", new mcp_js_1.ResourceTemplate("inkdrop://notes{?keyword,limit,skip,sort,descending}", { list: undefined }), async (uri) => { // forward query to /notes const qs = Object.fromEntries(uri.searchParams.entries()); const body = await fetchJSON("/notes", qs); return { contents: [ { uri: uri.href, text: JSON.stringify(body, null, 2) } ] }; }); // 2b) Create/update note (proxy POST /notes) server.tool("save-note", { _id: zod_1.z.string().optional(), _rev: zod_1.z.string().optional(), doctype: zod_1.z.string().optional(), bookId: zod_1.z.string().optional(), status: zod_1.z.string().optional(), share: zod_1.z.string().optional(), title: zod_1.z.string().optional(), body: zod_1.z.string().optional(), pinned: zod_1.z.boolean().optional(), tags: zod_1.z.array(zod_1.z.string()).optional() }, async (args) => { const ret = await postJSON("/notes", args); return { content: [{ type: "text", text: JSON.stringify(ret, null, 2) }] }; }); // 2c) Get single note (proxy GET /note:docId) server.resource("single-note", new mcp_js_1.ResourceTemplate("inkdrop://note/{docId}{?rev,attachments}", { list: undefined }), async (uri, { docId }) => { const qs = Object.fromEntries(uri.searchParams.entries()); const body = await fetchJSON(`/note:${docId}`, qs); return { contents: [ { uri: uri.href, text: JSON.stringify(body, null, 2) } ] }; }); // 2d) Delete note (proxy DELETE /note:docId) server.tool("delete-note", { docId: zod_1.z.string() }, async ({ docId }) => { const ret = await deleteJSON(`/note:${docId}`); return { content: [{ type: "text", text: JSON.stringify(ret, null, 2) }] }; }); // // 3) Books (Notebooks) // // 3a) List notebooks (GET /books) server.resource("books", new mcp_js_1.ResourceTemplate("inkdrop://books{?limit,skip}", { list: undefined }), async (uri) => { const qs = Object.fromEntries(uri.searchParams.entries()); const body = await fetchJSON("/books", qs); return { contents: [ { uri: uri.href, text: JSON.stringify(body, null, 2) } ] }; }); // 3b) Create/update a book (POST /books) server.tool("save-book", { _id: zod_1.z.string().optional(), _rev: zod_1.z.string().optional(), name: zod_1.z.string().optional(), parentBookId: zod_1.z.string().optional() }, async (args) => { const ret = await postJSON("/books", args); return { content: [{ type: "text", text: JSON.stringify(ret, null, 2) }] }; }); // 3c) Get a single book (GET /book:docId) server.resource("single-book", new mcp_js_1.ResourceTemplate("inkdrop://book/{docId}{?rev,attachments}", { list: undefined }), async (uri, { docId }) => { const qs = Object.fromEntries(uri.searchParams.entries()); const body = await fetchJSON(`/book:${docId}`, qs); return { contents: [ { uri: uri.href, text: JSON.stringify(body, null, 2) } ] }; }); // 3d) Delete book (DELETE /book:docId) server.tool("delete-book", { docId: zod_1.z.string() }, async ({ docId }) => { const ret = await deleteJSON(`/book:${docId}`); return { content: [{ type: "text", text: JSON.stringify(ret, null, 2) }] }; }); // // 4) Tags // // 4a) List tags (GET /tags) server.resource("tags", new mcp_js_1.ResourceTemplate("inkdrop://tags{?limit,skip}", { list: undefined }), async (uri) => { const qs = Object.fromEntries(uri.searchParams.entries()); const body = await fetchJSON("/tags", qs); return { contents: [ { uri: uri.href, text: JSON.stringify(body, null, 2) } ] }; }); // 4b) Create/update tag (POST /tags) server.tool("save-tag", { _id: zod_1.z.string().optional(), _rev: zod_1.z.string().optional(), name: zod_1.z.string(), color: zod_1.z.string().optional() }, async (args) => { const ret = await postJSON("/tags", args); return { content: [{ type: "text", text: JSON.stringify(ret, null, 2) }] }; }); // 4c) Get tag (GET /tag:docId) server.resource("single-tag", new mcp_js_1.ResourceTemplate("inkdrop://tag/{docId}{?rev,attachments}", { list: undefined }), async (uri, { docId }) => { const qs = Object.fromEntries(uri.searchParams.entries()); const body = await fetchJSON(`/tag:${docId}`, qs); return { contents: [ { uri: uri.href, text: JSON.stringify(body, null, 2) } ] }; }); // 4d) Delete tag (DELETE /tag:docId) server.tool("delete-tag", { docId: zod_1.z.string() }, async ({ docId }) => { const ret = await deleteJSON(`/tag:${docId}`); return { content: [{ type: "text", text: JSON.stringify(ret, null, 2) }] }; }); // // 5) Files // // 5a) List files (GET /files) server.resource("files", new mcp_js_1.ResourceTemplate("inkdrop://files{?limit,skip}", { list: undefined }), async (uri) => { const qs = Object.fromEntries(uri.searchParams.entries()); const body = await fetchJSON("/files", qs); return { contents: [ { uri: uri.href, text: JSON.stringify(body, null, 2) } ] }; }); // 5b) Create/update file (POST /files) server.tool("save-file", { _id: zod_1.z.string().optional(), _rev: zod_1.z.string().optional(), name: zod_1.z.string(), contentType: zod_1.z.string().optional(), contentLength: zod_1.z.number().optional(), publicIn: zod_1.z.array(zod_1.z.string()).optional(), _attachments: zod_1.z.any().optional() }, async (args) => { const ret = await postJSON("/files", args); return { content: [{ type: "text", text: JSON.stringify(ret, null, 2) }] }; }); // 5c) Get file (GET /file:docId) server.resource("single-file", new mcp_js_1.ResourceTemplate("inkdrop://file/{docId}{?rev,attachments}", { list: undefined }), async (uri, { docId }) => { const qs = Object.fromEntries(uri.searchParams.entries()); const body = await fetchJSON(`/file:${docId}`, qs); return { contents: [ { uri: uri.href, text: JSON.stringify(body, null, 2) } ] }; }); // 5d) Delete file (DELETE /file:docId) server.tool("delete-file", { docId: zod_1.z.string() }, async ({ docId }) => { const ret = await deleteJSON(`/file:${docId}`); return { content: [{ type: "text", text: JSON.stringify(ret, null, 2) }] }; }); // // 6) Changes feed (GET /_changes) // server.resource("changes", new mcp_js_1.ResourceTemplate("inkdrop://changes{?descending,since,limit,include_docs,conflicts,attachments}", { list: undefined }), async (uri) => { const qs = Object.fromEntries(uri.searchParams.entries()); const body = await fetchJSON("/_changes", qs); return { contents: [ { uri: uri.href, text: JSON.stringify(body, null, 2) } ] }; }); // Done! We have a resource/tool for every Inkdrop endpoint. return server; } /** Actually run an HTTP server exposing this MCP server over SSE and POST. */ async function main() { const mcpServer = createInkdropProxyServer(); const transport = new stdio_js_1.StdioServerTransport(); console.log("MCP (stdio) proxy for Inkdrop is now running..."); console.log("Send MCP requests on stdin, responses come out on stdout.\n"); await mcpServer.connect(transport); } main().catch((err) => { console.error("Error starting proxy:", err); process.exit(1); });