mirror of
https://github.com/bitbonsai/mcpvault.git
synced 2026-09-19 07:37:47 +08:00
feat: preview MCP SDK v2 dual-era support
This commit is contained in:
committed by
bitbonsai
parent
3a14634153
commit
071678e52a
@@ -0,0 +1,21 @@
|
||||
# MCP SDK v2 preview
|
||||
|
||||
> Experimental branch. Not published to npm.
|
||||
|
||||
This branch tracks MCPVault's migration to the MCP 2026-07-28 specification and the TypeScript SDK v2.
|
||||
|
||||
## Working now
|
||||
|
||||
- TypeScript SDK v2 package split
|
||||
- Dual-era stdio server: legacy MCP and 2026-07-28
|
||||
- Protocol matrix tests for both eras
|
||||
- Existing MCPVault test suite and security audit
|
||||
|
||||
## Before release
|
||||
|
||||
- Let the SDK v2 patch line and client ecosystem settle
|
||||
- Rebase current MCPVault feature work
|
||||
- Replace the experimental HTTP session layer with the stateless v2 handler
|
||||
- Verify legacy and modern HTTP clients through the MCP Inspector
|
||||
|
||||
Production users should continue installing `@bitbonsai/mcpvault@latest`.
|
||||
Vendored
+5
-5
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env node
|
||||
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
||||
import { serveStdio } from "@modelcontextprotocol/server/stdio";
|
||||
import { createServer } from "./src/createServer.js";
|
||||
import { parseCliArgs } from "./src/cli.js";
|
||||
import { readFileSync } from "fs";
|
||||
@@ -50,9 +50,9 @@ Examples:
|
||||
// unquoted vault paths with spaces. When omitted, use the current directory.
|
||||
const { vaultPathArg, readOnly } = parseCliArgs(cliArgs);
|
||||
const vaultPath = resolve(vaultPathArg || process.cwd());
|
||||
const server = createServer(vaultPath, { version: VERSION, readOnly });
|
||||
const transport = new StdioServerTransport();
|
||||
await server.connect(transport);
|
||||
// Serve both the legacy handshake-based protocol and MCP 2026-07-28 from the
|
||||
// same process. The opening exchange selects the era for this connection.
|
||||
const serverHandle = serveStdio(() => createServer(vaultPath, { version: VERSION, readOnly }), { onerror: (error) => console.error(error) });
|
||||
// Exit when the client disconnects (stdin EOF) or the process is asked to
|
||||
// terminate. Hosts that don't send an MCP shutdown request otherwise leave
|
||||
// this process running forever, orphaned once stdin closes (#159).
|
||||
@@ -62,7 +62,7 @@ async function shutdown() {
|
||||
return;
|
||||
isShuttingDown = true;
|
||||
try {
|
||||
await server.close();
|
||||
await serverHandle.close();
|
||||
}
|
||||
catch {
|
||||
// Best-effort: exit regardless of transport close errors.
|
||||
|
||||
Vendored
+1
-1
@@ -1,4 +1,4 @@
|
||||
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
||||
import { Server } from "@modelcontextprotocol/server";
|
||||
import { FrontmatterHandler } from "./frontmatter.js";
|
||||
import { PathFilter } from "./pathfilter.js";
|
||||
export interface CreateServerOptions {
|
||||
|
||||
Vendored
+1
-1
@@ -1 +1 @@
|
||||
{"version":3,"file":"createServer.d.ts","sourceRoot":"","sources":["../../src/createServer.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,2CAA2C,CAAC;AAMnE,OAAO,EAAE,kBAAkB,EAAoB,MAAM,kBAAkB,CAAC;AACxE,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAK7C,MAAM,WAAW,mBAAmB;IAClC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,UAAU,CAAC,EAAE,UAAU,CAAC;IACxB,kBAAkB,CAAC,EAAE,kBAAkB,CAAC;IACxC,wEAAwE;IACxE,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAYD,wBAAgB,YAAY,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,GAAE,mBAAwB,GAAG,MAAM,CAsezF"}
|
||||
{"version":3,"file":"createServer.d.ts","sourceRoot":"","sources":["../../src/createServer.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAa,MAAM,8BAA8B,CAAC;AAEjE,OAAO,EAAE,kBAAkB,EAAoB,MAAM,kBAAkB,CAAC;AACxE,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAK7C,MAAM,WAAW,mBAAmB;IAClC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,UAAU,CAAC,EAAE,UAAU,CAAC;IACxB,kBAAkB,CAAC,EAAE,kBAAkB,CAAC;IACxC,wEAAwE;IACxE,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAYD,wBAAgB,YAAY,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,GAAE,mBAAwB,GAAG,MAAM,CAsezF"}
|
||||
Vendored
+3
-4
@@ -1,5 +1,4 @@
|
||||
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
||||
import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
||||
import { Server } from "@modelcontextprotocol/server";
|
||||
import { FileSystemService } from "./filesystem.js";
|
||||
import { FrontmatterHandler, parseFrontmatter } from "./frontmatter.js";
|
||||
import { PathFilter } from "./pathfilter.js";
|
||||
@@ -23,7 +22,7 @@ export function createServer(vaultPath, options = {}) {
|
||||
const server = new Server({ name, version }, {
|
||||
capabilities: { tools: {} },
|
||||
});
|
||||
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
||||
server.setRequestHandler("tools/list", async () => {
|
||||
const tools = [
|
||||
{
|
||||
name: "read_note",
|
||||
@@ -272,7 +271,7 @@ export function createServer(vaultPath, options = {}) {
|
||||
: tools,
|
||||
};
|
||||
});
|
||||
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
||||
server.setRequestHandler("tools/call", async (request) => {
|
||||
const { name: toolName, arguments: args } = request.params;
|
||||
const trimmedArgs = trimPaths(args);
|
||||
if (readOnly && MUTATING_TOOLS.has(toolName)) {
|
||||
|
||||
Vendored
+1
-1
@@ -1,4 +1,4 @@
|
||||
import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
|
||||
import type { CallToolResult } from '@modelcontextprotocol/server';
|
||||
import type { FileSystemService } from '../filesystem.js';
|
||||
export interface WikiLinkToolArgs {
|
||||
document: string;
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
{"version":3,"file":"wikiLinkTool.d.ts","sourceRoot":"","sources":["../../../src/wikilink/wikiLinkTool.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oCAAoC,CAAC;AACzE,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAG1D,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB;AAED;;;;;;;;;;;;GAYG;AACH,wBAAsB,kBAAkB,CACtC,UAAU,EAAE,iBAAiB,EAC7B,IAAI,EAAE,gBAAgB,GACrB,OAAO,CAAC,cAAc,CAAC,CAiDzB"}
|
||||
{"version":3,"file":"wikiLinkTool.d.ts","sourceRoot":"","sources":["../../../src/wikilink/wikiLinkTool.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,8BAA8B,CAAC;AACnE,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAG1D,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB;AAED;;;;;;;;;;;;GAYG;AACH,wBAAsB,kBAAkB,CACtC,UAAU,EAAE,iBAAiB,EAC7B,IAAI,EAAE,gBAAgB,GACrB,OAAO,CAAC,cAAc,CAAC,CAiDzB"}
|
||||
Generated
+264
-1425
File diff suppressed because it is too large
Load Diff
+2
-1
@@ -39,13 +39,14 @@
|
||||
"ship:update": "bun scripts/ship.ts --update"
|
||||
},
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "^1.30.0",
|
||||
"@modelcontextprotocol/server": "^2.0.0",
|
||||
"gray-matter": "^4.0.3",
|
||||
"trash": "^10.1.1",
|
||||
"yaml": "^2.9.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@clack/prompts": "^0.7.0",
|
||||
"@modelcontextprotocol/client": "^2.0.0",
|
||||
"@types/node": "^26.1.2",
|
||||
"tsx": "^4.23.9",
|
||||
"typescript": "^7.0.2",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
||||
import { serveStdio } from "@modelcontextprotocol/server/stdio";
|
||||
import { createServer } from "./src/createServer.js";
|
||||
import { parseCliArgs } from "./src/cli.js";
|
||||
import { readFileSync } from "fs";
|
||||
@@ -59,9 +59,12 @@ Examples:
|
||||
const { vaultPathArg, readOnly } = parseCliArgs(cliArgs);
|
||||
const vaultPath = resolve(vaultPathArg || process.cwd());
|
||||
|
||||
const server = createServer(vaultPath, { version: VERSION, readOnly });
|
||||
const transport = new StdioServerTransport();
|
||||
await server.connect(transport);
|
||||
// Serve both the legacy handshake-based protocol and MCP 2026-07-28 from the
|
||||
// same process. The opening exchange selects the era for this connection.
|
||||
const serverHandle = serveStdio(
|
||||
() => createServer(vaultPath, { version: VERSION, readOnly }),
|
||||
{ onerror: (error) => console.error(error) },
|
||||
);
|
||||
|
||||
// Exit when the client disconnects (stdin EOF) or the process is asked to
|
||||
// terminate. Hosts that don't send an MCP shutdown request otherwise leave
|
||||
@@ -71,7 +74,7 @@ async function shutdown() {
|
||||
if (isShuttingDown) return;
|
||||
isShuttingDown = true;
|
||||
try {
|
||||
await server.close();
|
||||
await serverHandle.close();
|
||||
} catch {
|
||||
// Best-effort: exit regardless of transport close errors.
|
||||
}
|
||||
|
||||
@@ -3,8 +3,7 @@ import { createServer } from "./createServer.js";
|
||||
import { mkdtemp, mkdir, readFile, rm, writeFile } from "fs/promises";
|
||||
import { join } from "path";
|
||||
import { tmpdir } from "os";
|
||||
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
||||
import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
|
||||
import { Client, InMemoryTransport } from "@modelcontextprotocol/client";
|
||||
|
||||
let testVaultPath: string;
|
||||
|
||||
|
||||
+4
-8
@@ -1,8 +1,4 @@
|
||||
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
||||
import {
|
||||
CallToolRequestSchema,
|
||||
ListToolsRequestSchema,
|
||||
} from "@modelcontextprotocol/sdk/types.js";
|
||||
import { Server, type Tool } from "@modelcontextprotocol/server";
|
||||
import { FileSystemService } from "./filesystem.js";
|
||||
import { FrontmatterHandler, parseFrontmatter } from "./frontmatter.js";
|
||||
import { PathFilter } from "./pathfilter.js";
|
||||
@@ -46,8 +42,8 @@ export function createServer(vaultPath: string, options: CreateServerOptions = {
|
||||
capabilities: { tools: {} },
|
||||
});
|
||||
|
||||
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
||||
const tools = [
|
||||
server.setRequestHandler("tools/list", async () => {
|
||||
const tools: Tool[] = [
|
||||
{
|
||||
name: "read_note",
|
||||
description: "Read a note from the Obsidian vault",
|
||||
@@ -297,7 +293,7 @@ export function createServer(vaultPath: string, options: CreateServerOptions = {
|
||||
};
|
||||
});
|
||||
|
||||
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
||||
server.setRequestHandler("tools/call", async (request) => {
|
||||
const { name: toolName, arguments: args } = request.params;
|
||||
const trimmedArgs = trimPaths(args);
|
||||
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
import { afterEach, expect, test } from "vitest";
|
||||
import { Client } from "@modelcontextprotocol/client";
|
||||
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
|
||||
import { mkdtemp, rm } from "fs/promises";
|
||||
import { tmpdir } from "os";
|
||||
import { dirname, join } from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const repoRoot = join(__dirname, "..");
|
||||
const builtServer = join(repoRoot, "dist", "server.js");
|
||||
const vaults: string[] = [];
|
||||
|
||||
afterEach(async () => {
|
||||
await Promise.all(vaults.splice(0).map((vault) => rm(vault, { recursive: true, force: true })));
|
||||
});
|
||||
|
||||
async function connect(mode: "legacy" | "modern") {
|
||||
const vault = await mkdtemp(join(tmpdir(), `mcpvault-${mode}-`));
|
||||
vaults.push(vault);
|
||||
|
||||
const transport = new StdioClientTransport({
|
||||
command: process.execPath,
|
||||
args: [builtServer, vault],
|
||||
cwd: repoRoot,
|
||||
stderr: "pipe",
|
||||
});
|
||||
const client = mode === "modern"
|
||||
? new Client(
|
||||
{ name: "mcpvault-protocol-test", version: "1.0.0" },
|
||||
{ versionNegotiation: { mode: { pin: "2026-07-28" } } },
|
||||
)
|
||||
: new Client({ name: "mcpvault-protocol-test", version: "1.0.0" });
|
||||
|
||||
await client.connect(transport);
|
||||
return client;
|
||||
}
|
||||
|
||||
test("serves legacy handshake-based MCP clients", async () => {
|
||||
const client = await connect("legacy");
|
||||
try {
|
||||
const result = await client.listTools();
|
||||
expect(client.getProtocolEra()).toBe("legacy");
|
||||
expect(result.tools).toHaveLength(18);
|
||||
expect(result.tools[0]?.name).toBe("read_note");
|
||||
} finally {
|
||||
await client.close();
|
||||
}
|
||||
}, 15_000);
|
||||
|
||||
test("serves MCP 2026-07-28 clients", async () => {
|
||||
const client = await connect("modern");
|
||||
try {
|
||||
const result = await client.listTools();
|
||||
expect(client.getProtocolEra()).toBe("modern");
|
||||
expect(result.tools).toHaveLength(18);
|
||||
expect(result.tools[0]?.name).toBe("read_note");
|
||||
} finally {
|
||||
await client.close();
|
||||
}
|
||||
}, 15_000);
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
|
||||
import type { CallToolResult } from '@modelcontextprotocol/server';
|
||||
import type { FileSystemService } from '../filesystem.js';
|
||||
import { parseWikiLink } from './resolveWikiLink.js';
|
||||
|
||||
|
||||
Reference in New Issue
Block a user