supermemory/apps/mcp/e2e/discovery.test.ts
MaheshtheDev 203fb6e482 fix(mcp): keep pre-snake_case tool names as deprecated aliases (#1782)
#1665 renamed getDocument, listSpaces, listDocuments, listMemories and whoAmI to snake_case with no aliases. Clients with a cached tool list (ChatGPT, Codex, Claude) still call the old names and fail 100% of the time (~2k errors from ~300 users in the last 10 days, per PostHog). The old names are now registered again with the same handler and a "Deprecated alias" description. They stay listed because ChatGPT rejects calls to tools missing from tools/list.
2026-10-07 18:51:36 +00:00

115 lines
2.8 KiB
TypeScript

import { afterAll, beforeAll, describe, expect, it } from "vitest"
import {
OAUTH_CREDENTIALS_AVAILABLE,
callTool,
connect,
textOf,
type Session,
} from "./helpers"
const EXPECTED_TOOLS = [
"add_memory",
"fetch-graph-data",
"getDocument",
"get_document",
"get_profile",
"guided-save",
"listDocuments",
"listMemories",
"listSpaces",
"list_documents",
"list_memories",
"list_spaces",
"memory-graph",
"prepare-file-upload",
"save-memory",
"search_memory",
"select-space",
"set-active-tag",
"upload-file",
"whoAmI",
"who_am_i",
]
const describeWithAuth = describe.skipIf(!OAUTH_CREDENTIALS_AVAILABLE)
const READ_ONLY_TOOL_NAMES = [
"search_memory",
"get_profile",
"list_documents",
"list_memories",
"get_document",
"list_spaces",
"who_am_i",
"memory-graph",
]
const READ_ONLY_ANNOTATIONS = {
readOnlyHint: true,
destructiveHint: false,
idempotentHint: true,
openWorldHint: false,
}
const MEMORY_TOOL_ANNOTATIONS = {
readOnlyHint: false,
destructiveHint: true,
idempotentHint: false,
openWorldHint: false,
}
describeWithAuth("MCP — discovery & identity", () => {
let s: Session
beforeAll(async () => {
s = await connect()
})
afterAll(async () => {
await s?.close()
})
it("handshakes and lists the expected tools", async () => {
const { tools } = await s.client.listTools()
const names = tools.map((t) => t.name).sort()
expect(names).toEqual([...EXPECTED_TOOLS].sort())
})
it("marks read-only tools as non-destructive", async () => {
const { tools } = await s.client.listTools()
for (const name of READ_ONLY_TOOL_NAMES) {
const tool = tools.find((t) => t.name === name)
expect(tool?.annotations).toMatchObject(READ_ONLY_ANNOTATIONS)
}
})
it("marks addMemory as mutating", async () => {
const { tools } = await s.client.listTools()
const memory = tools.find((t) => t.name === "add_memory")
expect(memory?.annotations).toMatchObject(MEMORY_TOOL_ANNOTATIONS)
})
it("lists profile and space resources", async () => {
const { resources } = await s.client.listResources()
const uris = resources.map((r) => r.uri)
expect(uris).toContain("supermemory://profile")
expect(uris).toContain("supermemory://spaces")
})
it("lists the context prompt", async () => {
const { prompts } = await s.client.listPrompts()
expect(prompts.map((p) => p.name)).toContain("context")
})
it("whoAmI resolves to the authenticated account", async () => {
const res = await callTool(s.client, "who_am_i")
expect(res.isError).toBeFalsy()
const parsed = JSON.parse(textOf(res))
expect(parsed.userId).toBeTruthy()
expect(parsed).toHaveProperty("activeSpace")
})
it("listSpaces returns content", async () => {
const res = await callTool(s.client, "list_spaces")
expect(res.isError).toBeFalsy()
expect(textOf(res).length).toBeGreaterThan(0)
})
})