Files
template-app/test/mcp.test.ts
T

150 lines
5.8 KiB
TypeScript

// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
// Drive the same SDK handler with the official SDK client; authorization remains in the app caller.
import { buildRegistry, forbidden } from "@conusai-kit/core"
import { Client, StreamableHTTPClientTransport } from "@modelcontextprotocol/client"
import { describe, expect, it, vi } from "vitest"
import { createMcpHandler } from "../apps/web/src/server/mcp.js"
import { appModules } from "../modules/index.js"
const registry = buildRegistry([...appModules])
const principal = { method: "bearer", actor: { userId: "user-a" } } as never
function harness(
options: {
authenticated?: boolean
call?: (name: string, args: unknown) => Promise<unknown>
csrf?: () => void
} = {},
) {
const callByName = vi.fn(options.call ?? (async () => ({ notes: [], hasMore: false })))
const checkCsrf = vi.fn(options.csrf ?? (() => undefined))
const handle = createMcpHandler({
registry,
contextFor: async () => ({
principal: options.authenticated === false ? null : principal,
caller: { callByName } as never,
}),
checkCsrf,
})
return { handle, callByName, checkCsrf }
}
function rawRequest(body: unknown, extra: Record<string, string> = {}) {
return new Request("https://app.example.test/api/mcp", {
method: "POST",
headers: { "content-type": "application/json", accept: "application/json, text/event-stream", ...extra },
body: JSON.stringify(body),
})
}
function connect(handle: ReturnType<typeof createMcpHandler>, requestId = "sdk-request") {
const transport = new StreamableHTTPClientTransport(new URL("https://app.example.test/api/mcp"), {
fetch: (url, init) => handle(new Request(url, init), requestId),
})
const client = new Client({ name: "app-template-test", version: "1.0.0" }, { versionNegotiation: { mode: "auto" } })
return { client, transport }
}
describe("the stateless MCP endpoint", () => {
it("negotiates with the official client, lists opted-in tools, and calls through the scoped caller", async () => {
const output = { notes: [], hasMore: false }
const { handle, callByName, checkCsrf } = harness({ call: async () => output })
const { client, transport } = connect(handle)
try {
await client.connect(transport)
expect(client.getProtocolEra()).toBe("modern")
const listed = await client.listTools()
expect(listed.tools.map((tool) => tool.name)).toEqual(["agent.run.status", "knowledge.search", "notes.note.list"])
expect(listed.tools.find((tool) => tool.name === "notes.note.list")?.inputSchema.required).toEqual([
"org",
"workspace",
"project",
])
const result = await client.callTool({
name: "notes.note.list",
arguments: { org: "org-a", workspace: "ws-a", project: "project-a" },
})
expect(callByName).toHaveBeenCalledWith("notes.note.list", {
org: "org-a",
workspace: "ws-a",
project: "project-a",
})
expect(checkCsrf.mock.calls.length).toBeGreaterThanOrEqual(3)
expect(result.isError).not.toBe(true)
expect(result.structuredContent).toEqual(output)
} finally {
await client.close()
}
})
it("refuses unauthenticated requests before SDK protocol handling", async () => {
const { handle, callByName } = harness({ authenticated: false })
const response = await handle(rawRequest({ jsonrpc: "2.0", id: 4, method: "initialize" }), "request-4")
expect(response.status).toBe(401)
expect(response.headers.get("www-authenticate")).toBe("Bearer")
expect(callByName).not.toHaveBeenCalled()
expect(JSON.stringify(await response.json())).not.toContain("user-a")
})
it("applies the same session CSRF refusal and does not disclose its detail", async () => {
const { handle } = harness({
csrf: () => {
throw forbidden("sensitive csrf detail")
},
})
const response = await handle(rawRequest({ jsonrpc: "2.0", id: 5, method: "initialize" }), "request-5")
expect(response.status).toBe(403)
expect(JSON.stringify(await response.json())).not.toContain("sensitive csrf detail")
})
it("returns a safe tool error when kernel authorization refuses the caller", async () => {
const call = vi.fn(async (_name: string, args: unknown) => {
if (typeof args === "object" && args !== null && "org" in args && args.org === "other-org") {
throw forbidden("tenant-private note data")
}
return { notes: [], hasMore: false }
})
const { handle } = harness({
call,
})
const { client, transport } = connect(handle, "request-tool-refusal")
try {
await client.connect(transport)
const result = await client.callTool({
name: "notes.note.list",
arguments: { org: "other-org", workspace: "other-ws", project: "other-project" },
})
expect(result.isError).toBe(true)
expect(result.content[0]).toMatchObject({ type: "text", text: "The tool request was refused." })
expect(JSON.stringify(result)).not.toContain("tenant-private note data")
expect(call).toHaveBeenCalledWith("notes.note.list", {
org: "other-org",
workspace: "other-ws",
project: "other-project",
})
} finally {
await client.close()
}
})
it("refuses non-tool names and non-POST methods", async () => {
const { handle, callByName } = harness()
const { client, transport } = connect(handle, "request-7")
try {
await client.connect(transport)
await expect(client.callTool({ name: "notes.note.create", arguments: {} })).rejects.toMatchObject({
code: -32602,
})
expect(callByName).not.toHaveBeenCalled()
} finally {
await client.close()
}
const get = await handle(new Request("https://app.example.test/api/mcp"), "request-8")
expect(get.status).toBe(405)
})
})