@inkdropapp/mcp-server
Version:
Inkdrop Model Context Protocol Server
332 lines (331 loc) • 12.2 kB
JavaScript
"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);
});