-
Notifications
You must be signed in to change notification settings - Fork 1
Bring back MCP endpoint, scoped to the two /ask tools #246
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
b2ffe92
Bring back MCP endpoint, scoped to /ask tools
tomusdrw 5d964a2
fixup: format mcp handler import in api.ts
tomusdrw 3be42ca
Document /ask and /mcp endpoints in READMEs
tomusdrw caae504
MCP: polish schema, drop arrow wrappers, add server tests
tomusdrw d46da10
MCP: stateless, no CORS, no embeddings, single source of truth
tomusdrw 56bf9b3
CR: split error path in MCP tools, close test transports
tomusdrw File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,77 @@ | ||
| import { describe, expect, it } from "vitest"; | ||
| import { createApp } from "../../api.js"; | ||
| import { createSearchDB, insertDoc } from "../../data/searchIndex.js"; | ||
|
|
||
| function initBody() { | ||
| return { | ||
| jsonrpc: "2.0", | ||
| id: 1, | ||
| method: "initialize", | ||
| params: { | ||
| protocolVersion: "2025-03-26", | ||
| capabilities: {}, | ||
| clientInfo: { name: "handler-test", version: "0.0.1" }, | ||
| }, | ||
| }; | ||
| } | ||
|
|
||
| function mcpHeaders() { | ||
| return { | ||
| "Content-Type": "application/json", | ||
| Accept: "application/json, text/event-stream", | ||
| }; | ||
| } | ||
|
|
||
| describe("/mcp HTTP handler", () => { | ||
| it("handles initialize stateless (no CORS, no session id) and returns SSE body", async () => { | ||
| const db = createSearchDB(); | ||
| insertDoc(db, { | ||
| type: "graypaper_section", | ||
| title: "Accumulate", | ||
| content: "The accumulate function processes work results.", | ||
| }); | ||
| const app = createApp(db, "./data"); | ||
|
|
||
| const res = await app.fetch( | ||
| new Request("http://x/mcp", { | ||
| method: "POST", | ||
| headers: mcpHeaders(), | ||
| body: JSON.stringify(initBody()), | ||
| }) | ||
| ); | ||
|
|
||
| expect(res.status).toBe(200); | ||
| // CORS must not be advertised for server-to-server endpoint. | ||
| expect(res.headers.get("access-control-allow-origin")).toBeNull(); | ||
| // Stateless: transport must not mint a session id. | ||
| expect(res.headers.get("mcp-session-id")).toBeNull(); | ||
|
|
||
| const text = await res.text(); | ||
| expect(text).toContain("protocolVersion"); | ||
| expect(text).toContain("jam-search"); | ||
| }); | ||
|
|
||
| it("rejects initialize when Accept header is missing text/event-stream", async () => { | ||
| const app = createApp(createSearchDB(), "./data"); | ||
| const res = await app.fetch( | ||
| new Request("http://x/mcp", { | ||
| method: "POST", | ||
| headers: { "Content-Type": "application/json" }, | ||
| body: JSON.stringify(initBody()), | ||
| }) | ||
| ); | ||
| // MCP transport rejects with 406 when the client didn't advertise SSE. | ||
| expect(res.status).toBe(406); | ||
| }); | ||
|
|
||
| it("applies CORS to non-MCP routes (sanity check)", async () => { | ||
| const app = createApp(createSearchDB(), "./data"); | ||
| const res = await app.fetch( | ||
| new Request("http://x/health", { | ||
| headers: { Origin: "https://example.com" }, | ||
| }) | ||
| ); | ||
| expect(res.status).toBe(200); | ||
| expect(res.headers.get("access-control-allow-origin")).not.toBeNull(); | ||
| }); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,104 @@ | ||
| import { Client } from "@modelcontextprotocol/sdk/client/index.js"; | ||
| import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js"; | ||
| import { afterEach, describe, expect, it } from "vitest"; | ||
| import { createSearchDB, insertDoc } from "../../data/searchIndex.js"; | ||
| import { createMcpServer } from "../../mcp/server.js"; | ||
|
|
||
| describe("mcp server", () => { | ||
| const opened: Array<() => Promise<void>> = []; | ||
|
|
||
| afterEach(async () => { | ||
| await Promise.all(opened.map((close) => close())); | ||
| opened.length = 0; | ||
| }); | ||
|
|
||
| async function connectClient(db = createSearchDB()) { | ||
| const server = createMcpServer(db, "./data"); | ||
| const [clientTransport, serverTransport] = | ||
| InMemoryTransport.createLinkedPair(); | ||
| const client = new Client({ name: "test", version: "0.0.0" }); | ||
| await Promise.all([ | ||
| server.connect(serverTransport), | ||
| client.connect(clientTransport), | ||
| ]); | ||
| opened.push(async () => { | ||
| // close() is idempotent in the MCP SDK; closing both sides cleans up | ||
| // protocol state, transport handlers, and linked transport pair. | ||
| await Promise.all([client.close(), server.close()]); | ||
| }); | ||
| return { client, db }; | ||
| } | ||
|
|
||
| it("lists exactly the two /ask tools", async () => { | ||
| const { client } = await connectClient(); | ||
| const { tools } = await client.listTools(); | ||
| const names = tools.map((t) => t.name).sort(); | ||
| expect(names).toEqual(["get_full_document", "search_all"]); | ||
| }); | ||
|
|
||
| it("does not mark `limit` as required on search_all", async () => { | ||
| const { client } = await connectClient(); | ||
| const { tools } = await client.listTools(); | ||
| const searchAll = tools.find((t) => t.name === "search_all"); | ||
| expect(searchAll).toBeDefined(); | ||
| const required = (searchAll?.inputSchema as { required?: string[] }) | ||
| .required; | ||
| expect(required).toEqual(["query"]); | ||
| }); | ||
|
|
||
| it("calls search_all and returns a text content block", async () => { | ||
| const db = createSearchDB(); | ||
| insertDoc(db, { | ||
| type: "graypaper_section", | ||
| title: "Accumulate", | ||
| content: "The accumulate function processes work results.", | ||
| }); | ||
| const { client } = await connectClient(db); | ||
|
|
||
| const result = await client.callTool({ | ||
| name: "search_all", | ||
| arguments: { query: "accumulate" }, | ||
| }); | ||
|
|
||
| expect(result.isError).toBeFalsy(); | ||
| const content = result.content as Array<{ type: string; text: string }>; | ||
| expect(content[0].type).toBe("text"); | ||
| const parsed = JSON.parse(content[0].text) as Array<{ id: string }>; | ||
| expect(Array.isArray(parsed)).toBe(true); | ||
| expect(parsed.length).toBeGreaterThan(0); | ||
| expect(typeof parsed[0].id).toBe("string"); | ||
|
tomusdrw marked this conversation as resolved.
|
||
| }); | ||
|
|
||
| it("calls get_full_document and returns the doc body", async () => { | ||
| const db = createSearchDB(); | ||
| const id = insertDoc(db, { | ||
| type: "graypaper_section", | ||
| title: "Accumulate", | ||
| content: "Full body of the accumulate section...", | ||
| }); | ||
| const { client } = await connectClient(db); | ||
|
|
||
| const result = await client.callTool({ | ||
| name: "get_full_document", | ||
| arguments: { id }, | ||
| }); | ||
|
|
||
| expect(result.isError).toBeFalsy(); | ||
| const content = result.content as Array<{ type: string; text: string }>; | ||
| const parsed = JSON.parse(content[0].text) as { | ||
| id: string; | ||
| content: string; | ||
| }; | ||
| expect(parsed.id).toBe(id); | ||
| expect(parsed.content).toContain("Full body of the accumulate section"); | ||
| }); | ||
|
|
||
| it("returns isError for get_full_document with an unknown id", async () => { | ||
| const { client } = await connectClient(); | ||
| const result = await client.callTool({ | ||
| name: "get_full_document", | ||
| arguments: { id: "does-not-exist" }, | ||
| }); | ||
| expect(result.isError).toBe(true); | ||
| }); | ||
| }); | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.