@sobree/mcp 0.1.8 → 0.1.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/bin/sobree-mcp.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { StdioServerTransport as d } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
3
|
import * as w from "yjs";
|
|
4
|
-
import { c as u } from "../server-
|
|
4
|
+
import { c as u } from "../server-Bla2xZQ-.js";
|
|
5
5
|
async function f() {
|
|
6
6
|
const e = g(process.argv.slice(2)), s = new w.Doc();
|
|
7
7
|
let o = null;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sobree-mcp.js","sources":["../../src/bin/sobree-mcp.ts"],"sourcesContent":["#!/usr/bin/env node\n/**\n * `sobree-mcp` — stdio MCP server entry point.\n *\n * # Usage\n *\n * ```sh\n * # Local mode — LLM edits its own ephemeral Sobree doc.\n * npx @sobree/mcp\n *\n * # Collab mode — connect to a running @sobree/collab-server.\n * npx @sobree/mcp --ws-url ws://localhost:1234 --room doc-123\n * ```\n *\n * # Wiring into Claude Desktop\n *\n * Add to `~/Library/Application Support/Claude/claude_desktop_config.json`:\n *\n * ```json\n * {\n * \"mcpServers\": {\n * \"sobree\": {\n * \"command\": \"npx\",\n * \"args\": [\"-y\", \"@sobree/mcp\", \"--ws-url\", \"ws://localhost:1234\", \"--room\", \"demo\"]\n * }\n * }\n * }\n * ```\n *\n * Restart Claude Desktop. The model will see the Sobree tools listed\n * in its tool palette.\n *\n * # Flags\n *\n * --ws-url <url> WebSocket URL of a running @sobree/collab-server.\n * When set, the server connects to that room and\n * edits propagate to other peers.\n * --room <id> Room id to join (required with --ws-url).\n * --origin <name> Origin tag for this peer's mutations (default: \"mcp\").\n * --help Print usage and exit.\n */\n\nimport { StdioServerTransport } from \"@modelcontextprotocol/sdk/server/stdio.js\";\nimport * as Y from \"yjs\";\nimport { createSobreeMcpServer } from \"../server\";\n\ninterface Args {\n wsUrl?: string;\n room?: string;\n origin?: string;\n}\n\nasync function main(): Promise<void> {\n const args = parseArgs(process.argv.slice(2));\n\n const ydoc = new Y.Doc();\n\n // Optional collab mode — attach a y-websocket provider so this\n // peer's edits propagate to other clients in the same room.\n let provider: { destroy(): void; once(ev: string, cb: () => void): void } | null = null;\n if (args.wsUrl) {\n if (!args.room) {\n die(\"--room is required when --ws-url is set\");\n }\n try {\n const yws = await import(/* @vite-ignore */ \"y-websocket\");\n const Provider = (yws as { WebsocketProvider: unknown })
|
|
1
|
+
{"version":3,"file":"sobree-mcp.js","sources":["../../src/bin/sobree-mcp.ts"],"sourcesContent":["#!/usr/bin/env node\n/**\n * `sobree-mcp` — stdio MCP server entry point.\n *\n * # Usage\n *\n * ```sh\n * # Local mode — LLM edits its own ephemeral Sobree doc.\n * npx @sobree/mcp\n *\n * # Collab mode — connect to a running @sobree/collab-server.\n * npx @sobree/mcp --ws-url ws://localhost:1234 --room doc-123\n * ```\n *\n * # Wiring into Claude Desktop\n *\n * Add to `~/Library/Application Support/Claude/claude_desktop_config.json`:\n *\n * ```json\n * {\n * \"mcpServers\": {\n * \"sobree\": {\n * \"command\": \"npx\",\n * \"args\": [\"-y\", \"@sobree/mcp\", \"--ws-url\", \"ws://localhost:1234\", \"--room\", \"demo\"]\n * }\n * }\n * }\n * ```\n *\n * Restart Claude Desktop. The model will see the Sobree tools listed\n * in its tool palette.\n *\n * # Flags\n *\n * --ws-url <url> WebSocket URL of a running @sobree/collab-server.\n * When set, the server connects to that room and\n * edits propagate to other peers.\n * --room <id> Room id to join (required with --ws-url).\n * --origin <name> Origin tag for this peer's mutations (default: \"mcp\").\n * --help Print usage and exit.\n */\n\nimport { StdioServerTransport } from \"@modelcontextprotocol/sdk/server/stdio.js\";\nimport * as Y from \"yjs\";\nimport { createSobreeMcpServer } from \"../server\";\n\ninterface Args {\n wsUrl?: string;\n room?: string;\n origin?: string;\n}\n\nasync function main(): Promise<void> {\n const args = parseArgs(process.argv.slice(2));\n\n const ydoc = new Y.Doc();\n\n // Optional collab mode — attach a y-websocket provider so this\n // peer's edits propagate to other clients in the same room.\n let provider: { destroy(): void; once(ev: string, cb: () => void): void } | null = null;\n if (args.wsUrl) {\n if (!args.room) {\n die(\"--room is required when --ws-url is set\");\n }\n try {\n const yws = await import(/* @vite-ignore */ \"y-websocket\");\n const Provider = (yws as { WebsocketProvider: unknown }).WebsocketProvider as new (\n url: string,\n room: string,\n doc: Y.Doc,\n ) => {\n destroy(): void;\n once(ev: string, cb: () => void): void;\n };\n provider = new Provider(args.wsUrl, args.room, ydoc);\n // Wait briefly for initial sync so the LLM's first\n // get_document call returns hydrated state.\n await Promise.race([\n new Promise<void>((resolve) => provider?.once(\"sync\", resolve)),\n new Promise<void>((resolve) => setTimeout(resolve, 2000)),\n ]);\n log(`connected to ${args.wsUrl}/${args.room}`);\n } catch (err) {\n die(\n `failed to load y-websocket. Install it: pnpm add y-websocket. Original error: ${\n err instanceof Error ? err.message : String(err)\n }`,\n );\n }\n } else {\n log(\"local mode (no --ws-url) — Y.Doc is ephemeral\");\n }\n\n const { server, destroy } = createSobreeMcpServer({\n ydoc,\n ...(args.origin && { origin: args.origin }),\n });\n\n // Stdio transport — Claude Desktop and most MCP clients speak\n // this. The server reads JSON-RPC messages from stdin and writes\n // responses to stdout. Logging goes to stderr so it doesn't\n // corrupt the protocol stream.\n const transport = new StdioServerTransport();\n await server.connect(transport);\n\n log(\"ready\");\n\n // Graceful shutdown.\n const shutdown = async () => {\n try {\n destroy();\n provider?.destroy();\n ydoc.destroy();\n } catch {\n /* ignore */\n }\n process.exit(0);\n };\n process.on(\"SIGINT\", () => void shutdown());\n process.on(\"SIGTERM\", () => void shutdown());\n}\n\nfunction parseArgs(argv: readonly string[]): Args {\n const out: Args = {};\n for (let i = 0; i < argv.length; i++) {\n const arg = argv[i]!;\n if (arg === \"--help\" || arg === \"-h\") {\n printUsage();\n process.exit(0);\n } else if (arg === \"--ws-url\") {\n const v = argv[++i];\n if (v) out.wsUrl = v;\n } else if (arg === \"--room\") {\n const v = argv[++i];\n if (v) out.room = v;\n } else if (arg === \"--origin\") {\n const v = argv[++i];\n if (v) out.origin = v;\n } else {\n die(`unknown argument: ${arg}`);\n }\n }\n return out;\n}\n\nfunction printUsage(): void {\n process.stderr.write(`sobree-mcp — Sobree MCP server (stdio transport)\n\nUsage:\n sobree-mcp # local mode (ephemeral Y.Doc)\n sobree-mcp --ws-url <url> --room <id> # collab mode (join a @sobree/collab-server room)\n\nOptions:\n --ws-url <url> WebSocket URL of a @sobree/collab-server\n --room <id> Room id to join (required with --ws-url)\n --origin <name> Origin tag for this peer's mutations (default: \"mcp\")\n --help, -h Print this message\n`);\n}\n\nfunction log(msg: string): void {\n process.stderr.write(`[sobree-mcp] ${msg}\\n`);\n}\n\nfunction die(msg: string): never {\n process.stderr.write(`[sobree-mcp] error: ${msg}\\n`);\n process.exit(1);\n}\n\nmain().catch((err) => {\n die(err instanceof Error ? err.message : String(err));\n});\n"],"names":["main","args","parseArgs","ydoc","Y","provider","die","Provider","resolve","log","err","server","destroy","createSobreeMcpServer","transport","StdioServerTransport","shutdown","argv","out","i","arg","printUsage","v","msg"],"mappings":";;;;AAoDA,eAAeA,IAAsB;AACnC,QAAMC,IAAOC,EAAU,QAAQ,KAAK,MAAM,CAAC,CAAC,GAEtCC,IAAO,IAAIC,EAAE,IAAA;AAInB,MAAIC,IAA+E;AACnF,MAAIJ,EAAK,OAAO;AACd,IAAKA,EAAK,QACRK,EAAI,yCAAyC;AAE/C,QAAI;AAEF,YAAMC,KADM,MAAM;AAAA;AAAA,QAA0B;AAAA,MAAA,GACa;AAQzD,MAAAF,IAAW,IAAIE,EAASN,EAAK,OAAOA,EAAK,MAAME,CAAI,GAGnD,MAAM,QAAQ,KAAK;AAAA,QACjB,IAAI,QAAc,CAACK,MAAYH,GAAU,KAAK,QAAQG,CAAO,CAAC;AAAA,QAC9D,IAAI,QAAc,CAACA,MAAY,WAAWA,GAAS,GAAI,CAAC;AAAA,MAAA,CACzD,GACDC,EAAI,gBAAgBR,EAAK,KAAK,IAAIA,EAAK,IAAI,EAAE;AAAA,IAC/C,SAASS,GAAK;AACZ,MAAAJ;AAAA,QACE,iFACEI,aAAe,QAAQA,EAAI,UAAU,OAAOA,CAAG,CACjD;AAAA,MAAA;AAAA,IAEJ;AAAA,EACF;AACE,IAAAD,EAAI,+CAA+C;AAGrD,QAAM,EAAE,QAAAE,GAAQ,SAAAC,EAAA,IAAYC,EAAsB;AAAA,IAChD,MAAAV;AAAA,IACA,GAAIF,EAAK,UAAU,EAAE,QAAQA,EAAK,OAAA;AAAA,EAAO,CAC1C,GAMKa,IAAY,IAAIC,EAAA;AACtB,QAAMJ,EAAO,QAAQG,CAAS,GAE9BL,EAAI,OAAO;AAGX,QAAMO,IAAW,YAAY;AAC3B,QAAI;AACF,MAAAJ,EAAA,GACAP,GAAU,QAAA,GACVF,EAAK,QAAA;AAAA,IACP,QAAQ;AAAA,IAER;AACA,YAAQ,KAAK,CAAC;AAAA,EAChB;AACA,UAAQ,GAAG,UAAU,MAAM,KAAKa,GAAU,GAC1C,QAAQ,GAAG,WAAW,MAAM,KAAKA,GAAU;AAC7C;AAEA,SAASd,EAAUe,GAA+B;AAChD,QAAMC,IAAY,CAAA;AAClB,WAASC,IAAI,GAAGA,IAAIF,EAAK,QAAQE,KAAK;AACpC,UAAMC,IAAMH,EAAKE,CAAC;AAClB,QAAIC,MAAQ,YAAYA,MAAQ;AAC9B,MAAAC,EAAA,GACA,QAAQ,KAAK,CAAC;AAAA,aACLD,MAAQ,YAAY;AAC7B,YAAME,IAAIL,EAAK,EAAEE,CAAC;AAClB,MAAIG,QAAO,QAAQA;AAAA,IACrB,WAAWF,MAAQ,UAAU;AAC3B,YAAME,IAAIL,EAAK,EAAEE,CAAC;AAClB,MAAIG,QAAO,OAAOA;AAAA,IACpB,WAAWF,MAAQ,YAAY;AAC7B,YAAME,IAAIL,EAAK,EAAEE,CAAC;AAClB,MAAIG,QAAO,SAASA;AAAA,IACtB;AACE,MAAAhB,EAAI,qBAAqBc,CAAG,EAAE;AAAA,EAElC;AACA,SAAOF;AACT;AAEA,SAASG,IAAmB;AAC1B,UAAQ,OAAO,MAAM;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,CAWtB;AACD;AAEA,SAASZ,EAAIc,GAAmB;AAC9B,UAAQ,OAAO,MAAM,gBAAgBA,CAAG;AAAA,CAAI;AAC9C;AAEA,SAASjB,EAAIiB,GAAoB;AAC/B,UAAQ,OAAO,MAAM,uBAAuBA,CAAG;AAAA,CAAI,GACnD,QAAQ,KAAK,CAAC;AAChB;AAEAvB,IAAO,MAAM,CAACU,MAAQ;AACpB,EAAAJ,EAAII,aAAe,QAAQA,EAAI,UAAU,OAAOA,CAAG,CAAC;AACtD,CAAC;"}
|
package/dist/index.js
CHANGED
|
@@ -44,10 +44,7 @@ const y = {
|
|
|
44
44
|
required: ["afterBlockId", "text"]
|
|
45
45
|
},
|
|
46
46
|
handler: (e, t) => {
|
|
47
|
-
const o = a(e, t.afterBlockId, "afterBlockId"), r = e.insertBlockAfter(
|
|
48
|
-
o,
|
|
49
|
-
s([d(t.text)])
|
|
50
|
-
);
|
|
47
|
+
const o = a(e, t.afterBlockId, "afterBlockId"), r = e.insertBlockAfter(o, s([d(t.text)]));
|
|
51
48
|
if (!r.ok) throw new Error(c(r.error));
|
|
52
49
|
const n = r.affected?.[0];
|
|
53
50
|
if (!n) throw new Error("insertBlockAfter returned no new ref");
|
|
@@ -71,10 +68,7 @@ const y = {
|
|
|
71
68
|
required: ["beforeBlockId", "text"]
|
|
72
69
|
},
|
|
73
70
|
handler: (e, t) => {
|
|
74
|
-
const o = a(e, t.beforeBlockId, "beforeBlockId"), r = e.insertBlockBefore(
|
|
75
|
-
o,
|
|
76
|
-
s([d(t.text)])
|
|
77
|
-
);
|
|
71
|
+
const o = a(e, t.beforeBlockId, "beforeBlockId"), r = e.insertBlockBefore(o, s([d(t.text)]));
|
|
78
72
|
if (!r.ok) throw new Error(c(r.error));
|
|
79
73
|
const n = r.affected?.[0];
|
|
80
74
|
if (!n) throw new Error("insertBlockBefore returned no new ref");
|
|
@@ -221,4 +215,4 @@ export {
|
|
|
221
215
|
P as c,
|
|
222
216
|
T as f
|
|
223
217
|
};
|
|
224
|
-
//# sourceMappingURL=server-
|
|
218
|
+
//# sourceMappingURL=server-Bla2xZQ-.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server-Bla2xZQ-.js","sources":["../src/tools.ts","../src/server.ts"],"sourcesContent":["/**\n * Tool definitions for the Sobree MCP server.\n *\n * Each tool maps a Model Context Protocol invocation to a\n * `HeadlessSobree` mutation or read. Inputs are JSON Schema; outputs\n * are JSON-serialisable. The MCP transport layer (stdio, SSE, etc.)\n * is decoupled — these handlers are pure (input → output, plus side\n * effects on the supplied `HeadlessSobree`).\n *\n * # Design principles\n *\n * 1. **Small surface.** v0 ships the mutations LLMs actually need:\n * reading structure, inserting paragraphs, replacing paragraph\n * text, deleting blocks, setting alignment, undo/redo.\n * 2. **Block ids in / block ids out.** Every mutation takes a\n * `blockId` and returns the updated id list (or the new ref's\n * id). LLMs don't need to track versions — the optimistic-lock\n * check is server-side; on conflict the tool errors with a\n * message the LLM can act on.\n * 3. **Plain-text bias.** v0 mutations accept plain text. Rich\n * formatting (bold, color, headings) lives in the document\n * structure the LLM reads via `get_blocks` / `get_document`;\n * mutations that need formatting use `replace_paragraph` with\n * a typed `runs` payload (next iteration).\n */\n\nimport { type BlockInfo, type BlockRef, type HeadlessSobree, paragraph, text } from \"@sobree/core\";\n\n/**\n * Common JSON Schema for input validation. We use a minimal subset —\n * just enough for the MCP SDK to validate inputs before our handlers\n * run. The SDK ships its own validator.\n */\nexport interface ToolDefinition<I = unknown, O = unknown> {\n name: string;\n description: string;\n inputSchema: object;\n handler: (sobree: HeadlessSobree, input: I) => Promise<O> | O;\n}\n\n// === reads ===\n\nexport const getDocumentTool: ToolDefinition<Record<string, never>, { blocks: BlockInfo[] }> = {\n name: \"get_document\",\n description:\n \"Read the document as an array of block summaries. Each block has an id (use for subsequent mutations), kind (paragraph / section_break / table), a plain-text preview, and a character length. Call this first to understand the document's structure.\",\n inputSchema: {\n type: \"object\",\n properties: {},\n required: [],\n },\n handler: (sobree) => ({ blocks: sobree.getBlocks() }),\n};\n\nexport const getOutlineTool: ToolDefinition<\n Record<string, never>,\n { headings: Array<{ level: number; text: string; blockId: string }> }\n> = {\n name: \"get_outline\",\n description:\n \"Read the document's heading outline — one entry per heading paragraph (Heading1–Heading6), in document order. Useful for understanding section structure before making structural edits.\",\n inputSchema: {\n type: \"object\",\n properties: {},\n required: [],\n },\n handler: (sobree) => ({\n headings: sobree.getOutline().map((h) => ({\n level: h.level,\n text: h.text,\n blockId: h.block.id,\n })),\n }),\n};\n\n// === mutations ===\n\nexport const insertParagraphAfterTool: ToolDefinition<\n { afterBlockId: string; text: string },\n { blockId: string }\n> = {\n name: \"insert_paragraph_after\",\n description:\n \"Insert a new plain-text paragraph immediately after the block with the given id. Returns the new block's id so subsequent mutations can target it.\",\n inputSchema: {\n type: \"object\",\n properties: {\n afterBlockId: {\n type: \"string\",\n description: \"The id of the block to insert after. Get from get_document.\",\n },\n text: {\n type: \"string\",\n description: \"Plain text content for the new paragraph.\",\n },\n },\n required: [\"afterBlockId\", \"text\"],\n },\n handler: (sobree, input) => {\n const ref = lookupRef(sobree, input.afterBlockId, \"afterBlockId\");\n const result = sobree.insertBlockAfter(ref, paragraph([text(input.text)]));\n if (!result.ok) throw new Error(formatEditError(result.error));\n const newRef = result.affected?.[0];\n if (!newRef) throw new Error(\"insertBlockAfter returned no new ref\");\n return { blockId: newRef.id };\n },\n};\n\nexport const insertParagraphBeforeTool: ToolDefinition<\n { beforeBlockId: string; text: string },\n { blockId: string }\n> = {\n name: \"insert_paragraph_before\",\n description:\n \"Insert a new plain-text paragraph immediately before the block with the given id. Returns the new block's id.\",\n inputSchema: {\n type: \"object\",\n properties: {\n beforeBlockId: {\n type: \"string\",\n description: \"The id of the block to insert before.\",\n },\n text: {\n type: \"string\",\n description: \"Plain text content for the new paragraph.\",\n },\n },\n required: [\"beforeBlockId\", \"text\"],\n },\n handler: (sobree, input) => {\n const ref = lookupRef(sobree, input.beforeBlockId, \"beforeBlockId\");\n const result = sobree.insertBlockBefore(ref, paragraph([text(input.text)]));\n if (!result.ok) throw new Error(formatEditError(result.error));\n const newRef = result.affected?.[0];\n if (!newRef) throw new Error(\"insertBlockBefore returned no new ref\");\n return { blockId: newRef.id };\n },\n};\n\nexport const replaceParagraphTool: ToolDefinition<\n { blockId: string; text: string },\n { blockId: string }\n> = {\n name: \"replace_paragraph\",\n description:\n \"Replace the entire content of a paragraph block with new plain text. Drops any existing formatting (bold, color, etc.) in that paragraph. For preserving structure / formatting, read the block first, modify the runs, and use replace_paragraph_runs (future tool).\",\n inputSchema: {\n type: \"object\",\n properties: {\n blockId: { type: \"string\", description: \"Id of the paragraph to replace.\" },\n text: { type: \"string\", description: \"New plain text content.\" },\n },\n required: [\"blockId\", \"text\"],\n },\n handler: (sobree, input) => {\n const ref = lookupRef(sobree, input.blockId, \"blockId\");\n const result = sobree.replaceBlock(ref, paragraph([text(input.text)]));\n if (!result.ok) throw new Error(formatEditError(result.error));\n return { blockId: input.blockId };\n },\n};\n\nexport const deleteBlockTool: ToolDefinition<{ blockId: string }, { deleted: string }> = {\n name: \"delete_block\",\n description:\n \"Delete the block with the given id. If this would empty the document, an empty paragraph is left in its place.\",\n inputSchema: {\n type: \"object\",\n properties: {\n blockId: { type: \"string\", description: \"Id of the block to delete.\" },\n },\n required: [\"blockId\"],\n },\n handler: (sobree, input) => {\n const ref = lookupRef(sobree, input.blockId, \"blockId\");\n const result = sobree.deleteBlock(ref);\n if (!result.ok) throw new Error(formatEditError(result.error));\n return { deleted: input.blockId };\n },\n};\n\nexport const setParagraphAlignmentTool: ToolDefinition<\n { blockId: string; alignment: \"left\" | \"center\" | \"right\" | \"both\" | \"distribute\" },\n { blockId: string }\n> = {\n name: \"set_paragraph_alignment\",\n description:\n \"Set the text alignment of a paragraph block. `both` is OOXML's name for full justification.\",\n inputSchema: {\n type: \"object\",\n properties: {\n blockId: { type: \"string\" },\n alignment: {\n type: \"string\",\n enum: [\"left\", \"center\", \"right\", \"both\", \"distribute\"],\n },\n },\n required: [\"blockId\", \"alignment\"],\n },\n handler: (sobree, input) => {\n const ref = lookupRef(sobree, input.blockId, \"blockId\");\n const result = sobree.applyBlockProperties([ref], {\n alignment: input.alignment,\n });\n if (!result.ok) throw new Error(formatEditError(result.error));\n return { blockId: input.blockId };\n },\n};\n\n// === history ===\n\nexport const undoTool: ToolDefinition<Record<string, never>, { undone: boolean }> = {\n name: \"undo\",\n description:\n \"Reverse the most recent edit made by this MCP peer. Doesn't affect edits made by humans or other peers (per-peer undo via Y.UndoManager).\",\n inputSchema: { type: \"object\", properties: {}, required: [] },\n handler: (sobree) => ({ undone: sobree.history.undo() }),\n};\n\nexport const redoTool: ToolDefinition<Record<string, never>, { redone: boolean }> = {\n name: \"redo\",\n description: \"Re-apply the most recently undone edit by this MCP peer.\",\n inputSchema: { type: \"object\", properties: {}, required: [] },\n handler: (sobree) => ({ redone: sobree.history.redo() }),\n};\n\n// === registry ===\n\n/** The complete tool set in registration order. */\nexport const ALL_TOOLS: readonly ToolDefinition[] = [\n getDocumentTool,\n getOutlineTool,\n insertParagraphAfterTool,\n insertParagraphBeforeTool,\n replaceParagraphTool,\n deleteBlockTool,\n setParagraphAlignmentTool,\n undoTool,\n redoTool,\n] as ToolDefinition[];\n\n/** Look up a tool by name. Used by the MCP server's dispatch. */\nexport function findTool(name: string): ToolDefinition | undefined {\n return ALL_TOOLS.find((t) => t.name === name);\n}\n\n// === internals ===\n\n/**\n * Resolve a block id to a BlockRef (with current version) for the\n * editor's optimistic-lock check. Throws a user-facing error if the\n * id doesn't exist — the LLM may have referenced a deleted block.\n */\nfunction lookupRef(sobree: HeadlessSobree, blockId: string, field: string): BlockRef {\n const info = sobree.getBlockById(blockId);\n if (!info) {\n throw new Error(\n `${field}: block ${JSON.stringify(blockId)} not found. Call get_document to refresh block ids — the document may have changed.`,\n );\n }\n return { id: info.id, version: info.version };\n}\n\n/** Format an EditError as a human-readable string for the LLM. */\nfunction formatEditError(err: unknown): string {\n if (err && typeof err === \"object\" && \"code\" in err) {\n const e = err as { code: string; details?: string; conflicts?: unknown };\n if (e.code === \"optimistic-lock\") {\n return (\n \"optimistic-lock: another peer modified this block between your read and write. \" +\n \"Call get_document to refresh and try again.\"\n );\n }\n return `${e.code}${e.details ? `: ${e.details}` : \"\"}`;\n }\n return String(err);\n}\n","/**\n * `createSobreeMcpServer` — wires `HeadlessSobree` to an MCP `Server`\n * using the official `@modelcontextprotocol/sdk`.\n *\n * # Two operating modes\n *\n * - **Local** — the server holds its own Y.Doc, no provider. The\n * LLM is the only writer. Useful for \"give me a Sobree document\n * to dictate into\" workflows.\n *\n * - **Collab** — the caller passes a Y.Doc that's synced to a\n * collab-server (via `y-websocket`'s `WebsocketProvider`). The\n * LLM edits live alongside human peers. The server doesn't\n * manage the provider lifecycle — that's the caller's job (so\n * the same setup works in dev with one provider library and\n * prod with another).\n *\n * # Returns\n *\n * The server instance + the underlying `HeadlessSobree`. The MCP\n * transport (stdio, SSE, HTTP) is the caller's choice — `connect()`\n * the server to whichever they want. The CLI in\n * `bin/sobree-mcp.ts` ships a stdio default.\n */\n\nimport { Server } from \"@modelcontextprotocol/sdk/server/index.js\";\nimport { CallToolRequestSchema, ListToolsRequestSchema } from \"@modelcontextprotocol/sdk/types.js\";\nimport { HeadlessSobree, type HeadlessSobreeOptions, type SobreeDocument } from \"@sobree/core\";\nimport * as Y from \"yjs\";\nimport { ALL_TOOLS, findTool } from \"./tools\";\n\nexport interface CreateSobreeMcpServerOptions {\n /**\n * Y.Doc the server's HeadlessSobree should attach to. Pass one\n * with a provider (y-websocket etc.) already wired for collab\n * mode; pass none for local mode (the server creates its own\n * empty Y.Doc).\n */\n ydoc?: Y.Doc;\n /**\n * Initial document. Used only in local mode (when no `ydoc` is\n * supplied OR the supplied ydoc is empty). Same semantics as\n * `HeadlessSobree.initialDocument`.\n */\n initialDocument?: SobreeDocument;\n /**\n * Origin tag for this peer's mutations. Default `\"mcp\"`.\n * Visible to other peers via Y transaction origin; useful for\n * post-hoc telemetry (\"which edits came from the LLM\").\n */\n origin?: string;\n /**\n * Override the package's identity reported to the MCP client.\n * Defaults are usually fine.\n */\n serverInfo?: { name?: string; version?: string };\n /**\n * Pass-through to `HeadlessSobree`'s constructor. Use this for\n * the rare option not exposed at the top level.\n */\n headlessOptions?: Omit<HeadlessSobreeOptions, \"initialDocument\" | \"origin\">;\n}\n\nexport interface SobreeMcpServer {\n /** The underlying MCP server. Wire your transport with\n * `await server.connect(transport)`. */\n readonly server: Server;\n /** The HeadlessSobree peer the tools mutate. Read it for tests\n * or to drive non-MCP-mediated edits. */\n readonly sobree: HeadlessSobree;\n /** Tear down the headless peer + clear MCP state. The Y.Doc\n * itself is the caller's to dispose. */\n destroy(): void;\n}\n\nconst DEFAULT_NAME = \"sobree\";\nconst DEFAULT_VERSION = \"0.1.0\";\n\nexport function createSobreeMcpServer(opts: CreateSobreeMcpServerOptions = {}): SobreeMcpServer {\n const ydoc = opts.ydoc ?? new Y.Doc();\n const sobree = new HeadlessSobree(ydoc, {\n origin: opts.origin ?? \"mcp\",\n ...(opts.initialDocument ? { initialDocument: opts.initialDocument } : {}),\n ...opts.headlessOptions,\n });\n\n const server = new Server(\n {\n name: opts.serverInfo?.name ?? DEFAULT_NAME,\n version: opts.serverInfo?.version ?? DEFAULT_VERSION,\n },\n { capabilities: { tools: {} } },\n );\n\n // tools/list — return the static tool catalog. Names + schemas\n // come from the registry in `tools.ts`.\n server.setRequestHandler(ListToolsRequestSchema, async () => ({\n tools: ALL_TOOLS.map((t) => ({\n name: t.name,\n description: t.description,\n inputSchema: t.inputSchema,\n })),\n }));\n\n // tools/call — dispatch to the named handler. Errors are caught\n // and returned as content with `isError: true` per MCP spec, so\n // the LLM gets actionable feedback (e.g. \"blockId not found —\n // call get_document\").\n server.setRequestHandler(CallToolRequestSchema, async (request) => {\n const { name, arguments: args } = request.params;\n const tool = findTool(name);\n if (!tool) {\n return {\n content: [{ type: \"text\", text: `unknown tool: ${name}` }],\n isError: true,\n };\n }\n try {\n const result = await tool.handler(sobree, (args ?? {}) as never);\n return {\n content: [{ type: \"text\", text: JSON.stringify(result, null, 2) }],\n };\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n return {\n content: [{ type: \"text\", text: msg }],\n isError: true,\n };\n }\n });\n\n return {\n server,\n sobree,\n destroy(): void {\n sobree.destroy();\n },\n };\n}\n"],"names":["getDocumentTool","sobree","getOutlineTool","h","insertParagraphAfterTool","input","ref","lookupRef","result","paragraph","text","formatEditError","newRef","insertParagraphBeforeTool","replaceParagraphTool","deleteBlockTool","setParagraphAlignmentTool","undoTool","redoTool","ALL_TOOLS","findTool","name","blockId","field","info","err","e","DEFAULT_NAME","DEFAULT_VERSION","createSobreeMcpServer","opts","ydoc","Y","HeadlessSobree","server","Server","ListToolsRequestSchema","t","CallToolRequestSchema","request","args","tool"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sobree/mcp",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.10",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public"
|
|
6
6
|
},
|
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
},
|
|
56
56
|
"dependencies": {
|
|
57
57
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
58
|
-
"@sobree/core": "0.1.
|
|
58
|
+
"@sobree/core": "0.1.10"
|
|
59
59
|
},
|
|
60
60
|
"devDependencies": {
|
|
61
61
|
"@types/node": "^22.10.2",
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"server-Dk_FR_Dr.js","sources":["../src/tools.ts","../src/server.ts"],"sourcesContent":["/**\n * Tool definitions for the Sobree MCP server.\n *\n * Each tool maps a Model Context Protocol invocation to a\n * `HeadlessSobree` mutation or read. Inputs are JSON Schema; outputs\n * are JSON-serialisable. The MCP transport layer (stdio, SSE, etc.)\n * is decoupled — these handlers are pure (input → output, plus side\n * effects on the supplied `HeadlessSobree`).\n *\n * # Design principles\n *\n * 1. **Small surface.** v0 ships the mutations LLMs actually need:\n * reading structure, inserting paragraphs, replacing paragraph\n * text, deleting blocks, setting alignment, undo/redo.\n * 2. **Block ids in / block ids out.** Every mutation takes a\n * `blockId` and returns the updated id list (or the new ref's\n * id). LLMs don't need to track versions — the optimistic-lock\n * check is server-side; on conflict the tool errors with a\n * message the LLM can act on.\n * 3. **Plain-text bias.** v0 mutations accept plain text. Rich\n * formatting (bold, color, headings) lives in the document\n * structure the LLM reads via `get_blocks` / `get_document`;\n * mutations that need formatting use `replace_paragraph` with\n * a typed `runs` payload (next iteration).\n */\n\nimport {\n type BlockInfo,\n type BlockRef,\n type HeadlessSobree,\n paragraph,\n text,\n} from \"@sobree/core\";\n\n/**\n * Common JSON Schema for input validation. We use a minimal subset —\n * just enough for the MCP SDK to validate inputs before our handlers\n * run. The SDK ships its own validator.\n */\nexport interface ToolDefinition<I = unknown, O = unknown> {\n name: string;\n description: string;\n inputSchema: object;\n handler: (sobree: HeadlessSobree, input: I) => Promise<O> | O;\n}\n\n// === reads ===\n\nexport const getDocumentTool: ToolDefinition<\n Record<string, never>,\n { blocks: BlockInfo[] }\n> = {\n name: \"get_document\",\n description:\n \"Read the document as an array of block summaries. Each block has an id (use for subsequent mutations), kind (paragraph / section_break / table), a plain-text preview, and a character length. Call this first to understand the document's structure.\",\n inputSchema: {\n type: \"object\",\n properties: {},\n required: [],\n },\n handler: (sobree) => ({ blocks: sobree.getBlocks() }),\n};\n\nexport const getOutlineTool: ToolDefinition<\n Record<string, never>,\n { headings: Array<{ level: number; text: string; blockId: string }> }\n> = {\n name: \"get_outline\",\n description:\n \"Read the document's heading outline — one entry per heading paragraph (Heading1–Heading6), in document order. Useful for understanding section structure before making structural edits.\",\n inputSchema: {\n type: \"object\",\n properties: {},\n required: [],\n },\n handler: (sobree) => ({\n headings: sobree.getOutline().map((h) => ({\n level: h.level,\n text: h.text,\n blockId: h.block.id,\n })),\n }),\n};\n\n// === mutations ===\n\nexport const insertParagraphAfterTool: ToolDefinition<\n { afterBlockId: string; text: string },\n { blockId: string }\n> = {\n name: \"insert_paragraph_after\",\n description:\n \"Insert a new plain-text paragraph immediately after the block with the given id. Returns the new block's id so subsequent mutations can target it.\",\n inputSchema: {\n type: \"object\",\n properties: {\n afterBlockId: {\n type: \"string\",\n description: \"The id of the block to insert after. Get from get_document.\",\n },\n text: {\n type: \"string\",\n description: \"Plain text content for the new paragraph.\",\n },\n },\n required: [\"afterBlockId\", \"text\"],\n },\n handler: (sobree, input) => {\n const ref = lookupRef(sobree, input.afterBlockId, \"afterBlockId\");\n const result = sobree.insertBlockAfter(\n ref,\n paragraph([text(input.text)]),\n );\n if (!result.ok) throw new Error(formatEditError(result.error));\n const newRef = result.affected?.[0];\n if (!newRef) throw new Error(\"insertBlockAfter returned no new ref\");\n return { blockId: newRef.id };\n },\n};\n\nexport const insertParagraphBeforeTool: ToolDefinition<\n { beforeBlockId: string; text: string },\n { blockId: string }\n> = {\n name: \"insert_paragraph_before\",\n description:\n \"Insert a new plain-text paragraph immediately before the block with the given id. Returns the new block's id.\",\n inputSchema: {\n type: \"object\",\n properties: {\n beforeBlockId: {\n type: \"string\",\n description: \"The id of the block to insert before.\",\n },\n text: {\n type: \"string\",\n description: \"Plain text content for the new paragraph.\",\n },\n },\n required: [\"beforeBlockId\", \"text\"],\n },\n handler: (sobree, input) => {\n const ref = lookupRef(sobree, input.beforeBlockId, \"beforeBlockId\");\n const result = sobree.insertBlockBefore(\n ref,\n paragraph([text(input.text)]),\n );\n if (!result.ok) throw new Error(formatEditError(result.error));\n const newRef = result.affected?.[0];\n if (!newRef) throw new Error(\"insertBlockBefore returned no new ref\");\n return { blockId: newRef.id };\n },\n};\n\nexport const replaceParagraphTool: ToolDefinition<\n { blockId: string; text: string },\n { blockId: string }\n> = {\n name: \"replace_paragraph\",\n description:\n \"Replace the entire content of a paragraph block with new plain text. Drops any existing formatting (bold, color, etc.) in that paragraph. For preserving structure / formatting, read the block first, modify the runs, and use replace_paragraph_runs (future tool).\",\n inputSchema: {\n type: \"object\",\n properties: {\n blockId: { type: \"string\", description: \"Id of the paragraph to replace.\" },\n text: { type: \"string\", description: \"New plain text content.\" },\n },\n required: [\"blockId\", \"text\"],\n },\n handler: (sobree, input) => {\n const ref = lookupRef(sobree, input.blockId, \"blockId\");\n const result = sobree.replaceBlock(ref, paragraph([text(input.text)]));\n if (!result.ok) throw new Error(formatEditError(result.error));\n return { blockId: input.blockId };\n },\n};\n\nexport const deleteBlockTool: ToolDefinition<\n { blockId: string },\n { deleted: string }\n> = {\n name: \"delete_block\",\n description:\n \"Delete the block with the given id. If this would empty the document, an empty paragraph is left in its place.\",\n inputSchema: {\n type: \"object\",\n properties: {\n blockId: { type: \"string\", description: \"Id of the block to delete.\" },\n },\n required: [\"blockId\"],\n },\n handler: (sobree, input) => {\n const ref = lookupRef(sobree, input.blockId, \"blockId\");\n const result = sobree.deleteBlock(ref);\n if (!result.ok) throw new Error(formatEditError(result.error));\n return { deleted: input.blockId };\n },\n};\n\nexport const setParagraphAlignmentTool: ToolDefinition<\n { blockId: string; alignment: \"left\" | \"center\" | \"right\" | \"both\" | \"distribute\" },\n { blockId: string }\n> = {\n name: \"set_paragraph_alignment\",\n description:\n \"Set the text alignment of a paragraph block. `both` is OOXML's name for full justification.\",\n inputSchema: {\n type: \"object\",\n properties: {\n blockId: { type: \"string\" },\n alignment: {\n type: \"string\",\n enum: [\"left\", \"center\", \"right\", \"both\", \"distribute\"],\n },\n },\n required: [\"blockId\", \"alignment\"],\n },\n handler: (sobree, input) => {\n const ref = lookupRef(sobree, input.blockId, \"blockId\");\n const result = sobree.applyBlockProperties([ref], {\n alignment: input.alignment,\n });\n if (!result.ok) throw new Error(formatEditError(result.error));\n return { blockId: input.blockId };\n },\n};\n\n// === history ===\n\nexport const undoTool: ToolDefinition<Record<string, never>, { undone: boolean }> = {\n name: \"undo\",\n description:\n \"Reverse the most recent edit made by this MCP peer. Doesn't affect edits made by humans or other peers (per-peer undo via Y.UndoManager).\",\n inputSchema: { type: \"object\", properties: {}, required: [] },\n handler: (sobree) => ({ undone: sobree.history.undo() }),\n};\n\nexport const redoTool: ToolDefinition<Record<string, never>, { redone: boolean }> = {\n name: \"redo\",\n description: \"Re-apply the most recently undone edit by this MCP peer.\",\n inputSchema: { type: \"object\", properties: {}, required: [] },\n handler: (sobree) => ({ redone: sobree.history.redo() }),\n};\n\n// === registry ===\n\n/** The complete tool set in registration order. */\nexport const ALL_TOOLS: readonly ToolDefinition[] = [\n getDocumentTool,\n getOutlineTool,\n insertParagraphAfterTool,\n insertParagraphBeforeTool,\n replaceParagraphTool,\n deleteBlockTool,\n setParagraphAlignmentTool,\n undoTool,\n redoTool,\n] as ToolDefinition[];\n\n/** Look up a tool by name. Used by the MCP server's dispatch. */\nexport function findTool(name: string): ToolDefinition | undefined {\n return ALL_TOOLS.find((t) => t.name === name);\n}\n\n// === internals ===\n\n/**\n * Resolve a block id to a BlockRef (with current version) for the\n * editor's optimistic-lock check. Throws a user-facing error if the\n * id doesn't exist — the LLM may have referenced a deleted block.\n */\nfunction lookupRef(\n sobree: HeadlessSobree,\n blockId: string,\n field: string,\n): BlockRef {\n const info = sobree.getBlockById(blockId);\n if (!info) {\n throw new Error(\n `${field}: block ${JSON.stringify(blockId)} not found. ` +\n \"Call get_document to refresh block ids — the document may have changed.\",\n );\n }\n return { id: info.id, version: info.version };\n}\n\n/** Format an EditError as a human-readable string for the LLM. */\nfunction formatEditError(err: unknown): string {\n if (err && typeof err === \"object\" && \"code\" in err) {\n const e = err as { code: string; details?: string; conflicts?: unknown };\n if (e.code === \"optimistic-lock\") {\n return (\n \"optimistic-lock: another peer modified this block between your read and write. \" +\n \"Call get_document to refresh and try again.\"\n );\n }\n return `${e.code}${e.details ? `: ${e.details}` : \"\"}`;\n }\n return String(err);\n}\n","/**\n * `createSobreeMcpServer` — wires `HeadlessSobree` to an MCP `Server`\n * using the official `@modelcontextprotocol/sdk`.\n *\n * # Two operating modes\n *\n * - **Local** — the server holds its own Y.Doc, no provider. The\n * LLM is the only writer. Useful for \"give me a Sobree document\n * to dictate into\" workflows.\n *\n * - **Collab** — the caller passes a Y.Doc that's synced to a\n * collab-server (via `y-websocket`'s `WebsocketProvider`). The\n * LLM edits live alongside human peers. The server doesn't\n * manage the provider lifecycle — that's the caller's job (so\n * the same setup works in dev with one provider library and\n * prod with another).\n *\n * # Returns\n *\n * The server instance + the underlying `HeadlessSobree`. The MCP\n * transport (stdio, SSE, HTTP) is the caller's choice — `connect()`\n * the server to whichever they want. The CLI in\n * `bin/sobree-mcp.ts` ships a stdio default.\n */\n\nimport { Server } from \"@modelcontextprotocol/sdk/server/index.js\";\nimport {\n CallToolRequestSchema,\n ListToolsRequestSchema,\n} from \"@modelcontextprotocol/sdk/types.js\";\nimport {\n HeadlessSobree,\n type HeadlessSobreeOptions,\n type SobreeDocument,\n} from \"@sobree/core\";\nimport * as Y from \"yjs\";\nimport { ALL_TOOLS, findTool } from \"./tools\";\n\nexport interface CreateSobreeMcpServerOptions {\n /**\n * Y.Doc the server's HeadlessSobree should attach to. Pass one\n * with a provider (y-websocket etc.) already wired for collab\n * mode; pass none for local mode (the server creates its own\n * empty Y.Doc).\n */\n ydoc?: Y.Doc;\n /**\n * Initial document. Used only in local mode (when no `ydoc` is\n * supplied OR the supplied ydoc is empty). Same semantics as\n * `HeadlessSobree.initialDocument`.\n */\n initialDocument?: SobreeDocument;\n /**\n * Origin tag for this peer's mutations. Default `\"mcp\"`.\n * Visible to other peers via Y transaction origin; useful for\n * post-hoc telemetry (\"which edits came from the LLM\").\n */\n origin?: string;\n /**\n * Override the package's identity reported to the MCP client.\n * Defaults are usually fine.\n */\n serverInfo?: { name?: string; version?: string };\n /**\n * Pass-through to `HeadlessSobree`'s constructor. Use this for\n * the rare option not exposed at the top level.\n */\n headlessOptions?: Omit<\n HeadlessSobreeOptions,\n \"initialDocument\" | \"origin\"\n >;\n}\n\nexport interface SobreeMcpServer {\n /** The underlying MCP server. Wire your transport with\n * `await server.connect(transport)`. */\n readonly server: Server;\n /** The HeadlessSobree peer the tools mutate. Read it for tests\n * or to drive non-MCP-mediated edits. */\n readonly sobree: HeadlessSobree;\n /** Tear down the headless peer + clear MCP state. The Y.Doc\n * itself is the caller's to dispose. */\n destroy(): void;\n}\n\nconst DEFAULT_NAME = \"sobree\";\nconst DEFAULT_VERSION = \"0.1.0\";\n\nexport function createSobreeMcpServer(\n opts: CreateSobreeMcpServerOptions = {},\n): SobreeMcpServer {\n const ydoc = opts.ydoc ?? new Y.Doc();\n const sobree = new HeadlessSobree(ydoc, {\n origin: opts.origin ?? \"mcp\",\n ...(opts.initialDocument ? { initialDocument: opts.initialDocument } : {}),\n ...opts.headlessOptions,\n });\n\n const server = new Server(\n {\n name: opts.serverInfo?.name ?? DEFAULT_NAME,\n version: opts.serverInfo?.version ?? DEFAULT_VERSION,\n },\n { capabilities: { tools: {} } },\n );\n\n // tools/list — return the static tool catalog. Names + schemas\n // come from the registry in `tools.ts`.\n server.setRequestHandler(ListToolsRequestSchema, async () => ({\n tools: ALL_TOOLS.map((t) => ({\n name: t.name,\n description: t.description,\n inputSchema: t.inputSchema,\n })),\n }));\n\n // tools/call — dispatch to the named handler. Errors are caught\n // and returned as content with `isError: true` per MCP spec, so\n // the LLM gets actionable feedback (e.g. \"blockId not found —\n // call get_document\").\n server.setRequestHandler(CallToolRequestSchema, async (request) => {\n const { name, arguments: args } = request.params;\n const tool = findTool(name);\n if (!tool) {\n return {\n content: [{ type: \"text\", text: `unknown tool: ${name}` }],\n isError: true,\n };\n }\n try {\n const result = await tool.handler(sobree, (args ?? {}) as never);\n return {\n content: [{ type: \"text\", text: JSON.stringify(result, null, 2) }],\n };\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n return {\n content: [{ type: \"text\", text: msg }],\n isError: true,\n };\n }\n });\n\n return {\n server,\n sobree,\n destroy(): void {\n sobree.destroy();\n },\n };\n}\n"],"names":["getDocumentTool","sobree","getOutlineTool","h","insertParagraphAfterTool","input","ref","lookupRef","result","paragraph","text","formatEditError","newRef","insertParagraphBeforeTool","replaceParagraphTool","deleteBlockTool","setParagraphAlignmentTool","undoTool","redoTool","ALL_TOOLS","findTool","name","blockId","field","info","err","e","DEFAULT_NAME","DEFAULT_VERSION","createSobreeMcpServer","opts","ydoc","Y","HeadlessSobree","server","Server","ListToolsRequestSchema","t","CallToolRequestSchema","request","args","tool"],"mappings":"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"}
|