feat(mcp): use camelCase for public model tools

Claude and Codex prefix the server name, so search_memory became a
long snake pile. Public tools are now searchMemory, getProfile, and
addMemory to match listDocuments and whoAmI. Widget tools stay kebab-case.
This commit is contained in:
Dhravya 2026-09-12 23:27:52 +00:00
parent cc1167a867
commit 37ce918bd9
16 changed files with 53 additions and 55 deletions

View file

@ -46,9 +46,9 @@ Your assistant chooses these tools automatically. Use this table when you need t
| Tool | Use it for | Inputs | Result |
| --- | --- | --- | --- |
| `search_memory` | Semantic recall from one space | `query` (required), `containerTag` | Matching memories |
| `get_profile` | Stable and recent profile context for one space | `containerTag` | Profile facts |
| `add_memory` | Save information or forget outdated information | `content` (required), `action` (`save` or `forget`), `containerTag` | Save or forget confirmation |
| `searchMemory` | Semantic recall from one space | `query` (required), `containerTag` | Matching memories |
| `getProfile` | Stable and recent profile context for one space | `containerTag` | Profile facts |
| `addMemory` | Save information or forget outdated information | `content` (required), `action` (`save` or `forget`), `containerTag` | Save or forget confirmation |
| `listDocuments` | Browse stored source documents and their summaries | `page`, `limit`, `containerTag` | Document IDs, titles, types, status, dates, and summaries |
| `getDocument` | Read the available content of one document | `documentId` (required) | Document metadata, summary, and available content |
| `listMemories` | Browse recent extracted memory entries and their source document IDs | `page`, `limit`, `containerTag` | Memory IDs, text, versions, and source document IDs |
@ -57,12 +57,12 @@ Your assistant chooses these tools automatically. Use this table when you need t
### Search
`search_memory` accepts a natural-language query and returns semantically relevant memories. It does not include the space profile. Use `get_profile` when the assistant needs stable facts or recent context that search did not return.
`searchMemory` accepts a natural-language query and returns semantically relevant memories. It does not include the space profile. Use `getProfile` when the assistant needs stable facts or recent context that search did not return.
Use the retrieval tools for different questions:
- Use `search_memory` to answer a question from remembered context.
- Use `get_profile` for who-the-user-is, preferences, and recent context.
- Use `searchMemory` to answer a question from remembered context.
- Use `getProfile` for who-the-user-is, preferences, and recent context.
- Use `listDocuments` to discover stored sources, then `getDocument` to read one source in full.
- Use `listMemories` to inspect the extracted memory entries themselves, including their IDs and source document IDs.
@ -70,9 +70,9 @@ Use the retrieval tools for different questions:
### Save or forget
`add_memory` saves the supplied `content` by default. Set `action` to `forget` when a fact is outdated or should be removed. There is no separate forget tool.
`addMemory` saves the supplied `content` by default. Set `action` to `forget` when a fact is outdated or should be removed. There is no separate forget tool.
If the content is already final, the assistant should use `add_memory`. If you want to review, edit, or choose a space before saving, it should open the `guided-save` widget instead.
If the content is already final, the assistant should use `addMemory`. If you want to review, edit, or choose a space before saving, it should open the `guided-save` widget instead.
### Access control

View file

@ -48,14 +48,14 @@ The client discovers the OAuth authorization server through
| Tool | Purpose |
| --- | --- |
| `search_memory` | Search memories in a space |
| `get_profile` | Get stable and recent profile context for a space |
| `searchMemory` | Search memories in a space |
| `getProfile` | Get stable and recent profile context for a space |
| `listDocuments` | List document metadata and summaries in a space |
| `getDocument` | Read one document's available content by ID |
| `listMemories` | List extracted memory entries and their source document IDs |
| `listSpaces` | List spaces visible to the authenticated account |
| `whoAmI` | Return identity, access, and active-space context |
| `add_memory` | Save or forget a memory |
| `addMemory` | Save or forget a memory |
### MCP App launchers

View file

@ -8,10 +8,10 @@ import {
} from "./helpers"
const EXPECTED_TOOLS = [
"add_memory",
"addMemory",
"fetch-graph-data",
"getDocument",
"get_profile",
"getProfile",
"guided-save",
"listDocuments",
"listMemories",
@ -19,7 +19,7 @@ const EXPECTED_TOOLS = [
"memory-graph",
"prepare-file-upload",
"save-memory",
"search_memory",
"searchMemory",
"select-space",
"set-active-tag",
"upload-file",
@ -28,8 +28,8 @@ const EXPECTED_TOOLS = [
const describeWithAuth = describe.skipIf(!OAUTH_CREDENTIALS_AVAILABLE)
const READ_ONLY_TOOL_NAMES = [
"search_memory",
"get_profile",
"searchMemory",
"getProfile",
"listDocuments",
"listMemories",
"getDocument",
@ -76,9 +76,9 @@ describeWithAuth("MCP — discovery & identity", () => {
}
})
it("marks add_memory as mutating", async () => {
it("marks addMemory as mutating", async () => {
const { tools } = await s.client.listTools()
const memory = tools.find((t) => t.name === "add_memory")
const memory = tools.find((t) => t.name === "addMemory")
expect(memory?.annotations).toMatchObject(MEMORY_TOOL_ANNOTATIONS)
})

View file

@ -223,7 +223,7 @@ export async function recallUntil(
} = {},
): Promise<string | null> {
for (let i = 0; i < tries; i++) {
const res = await callTool(client, "search_memory", {
const res = await callTool(client, "searchMemory", {
query,
...(containerTag ? { containerTag } : {}),
})

View file

@ -54,7 +54,7 @@ describe.skipIf(!OAUTH_CREDENTIALS_AVAILABLE)(
afterAll(async () => {
for (const memory of createdMemories) {
await callTool(session.client, "add_memory", {
await callTool(session.client, "addMemory", {
content: memory.content,
action: "forget",
containerTag: memory.containerTag,

View file

@ -18,7 +18,7 @@ describe.skipIf(!OAUTH_CREDENTIALS_AVAILABLE)("MCP — memory behaviors", () =>
})
afterAll(async () => {
for (const { content, containerTag } of created) {
await callTool(s.client, "add_memory", {
await callTool(s.client, "addMemory", {
content,
action: "forget",
...(containerTag ? { containerTag } : {}),
@ -32,7 +32,7 @@ describe.skipIf(!OAUTH_CREDENTIALS_AVAILABLE)("MCP — memory behaviors", () =>
const content = `e2e round-trip. token=${marker}. The test fruit is dragonfruit.`
created.push({ content })
const save = await callTool(s.client, "add_memory", {
const save = await callTool(s.client, "addMemory", {
content,
action: "save",
})
@ -43,8 +43,8 @@ describe.skipIf(!OAUTH_CREDENTIALS_AVAILABLE)("MCP — memory behaviors", () =>
expect(found, `recall never returned marker ${marker}`).not.toBeNull()
}, 120_000)
it("search_memory does not include profile sections", async () => {
const res = await callTool(s.client, "search_memory", {
it("searchMemory does not include profile sections", async () => {
const res = await callTool(s.client, "searchMemory", {
query: "dragonfruit",
})
expect(res.isError).toBeFalsy()
@ -53,8 +53,8 @@ describe.skipIf(!OAUTH_CREDENTIALS_AVAILABLE)("MCP — memory behaviors", () =>
expect(txt).toMatch(/## Matching memories|No matching memories found/i)
}, 30_000)
it("get_profile returns profile sections", async () => {
const res = await callTool(s.client, "get_profile", {})
it("getProfile returns profile sections", async () => {
const res = await callTool(s.client, "getProfile", {})
expect(res.isError).toBeFalsy()
const txt = textOf(res)
expect(txt).toMatch(
@ -64,7 +64,7 @@ describe.skipIf(!OAUTH_CREDENTIALS_AVAILABLE)("MCP — memory behaviors", () =>
// Hybrid search returns nearest matches even for unrelated queries — assert it responds gracefully, not empty.
it("recall responds gracefully for an unmatched query", async () => {
const res = await callTool(s.client, "search_memory", {
const res = await callTool(s.client, "searchMemory", {
query: `zzz-no-such-memory-${randomUUID()}`,
})
expect(res.isError).toBeFalsy()
@ -78,11 +78,11 @@ describe.skipIf(!OAUTH_CREDENTIALS_AVAILABLE)("MCP — memory behaviors", () =>
const content = `e2e forget target. token=${marker}. Secret animal is axolotl.`
created.push({ content })
await callTool(s.client, "add_memory", { content, action: "save" })
await callTool(s.client, "addMemory", { content, action: "save" })
const found = await recallUntil(s.client, "secret animal axolotl", marker)
expect(found, "memory should exist before forget").not.toBeNull()
const forgotten = await callTool(s.client, "add_memory", {
const forgotten = await callTool(s.client, "addMemory", {
content,
action: "forget",
})
@ -98,7 +98,7 @@ describe.skipIf(!OAUTH_CREDENTIALS_AVAILABLE)("MCP — memory behaviors", () =>
const content = `e2e scoping. token=${marker}. Project color is teal.`
created.push({ content, containerTag: tagA })
await callTool(s.client, "add_memory", {
await callTool(s.client, "addMemory", {
content,
action: "save",
containerTag: tagA,
@ -119,7 +119,7 @@ describe.skipIf(!OAUTH_CREDENTIALS_AVAILABLE)("MCP — memory behaviors", () =>
}, 120_000)
it("returns an error result for a missing required argument", async () => {
const res = await callTool(s.client, "search_memory", {})
const res = await callTool(s.client, "searchMemory", {})
expect(res.isError).toBe(true)
expect(textOf(res).length).toBeGreaterThan(0)
})

View file

@ -21,17 +21,15 @@ describeWithAuth("MCP - space scoping", () => {
const scopedTools = (await scoped.client.listTools()).tools
const plainTools = (await plain.client.listTools()).tools
expect(propsOf(plainTools, "add_memory")).toHaveProperty("containerTag")
expect(propsOf(plainTools, "search_memory")).toHaveProperty(
"containerTag",
)
expect(propsOf(plainTools, "get_profile")).toHaveProperty("containerTag")
expect(propsOf(plainTools, "addMemory")).toHaveProperty("containerTag")
expect(propsOf(plainTools, "searchMemory")).toHaveProperty("containerTag")
expect(propsOf(plainTools, "getProfile")).toHaveProperty("containerTag")
expect(propsOf(scopedTools, "add_memory")).toHaveProperty("containerTag")
expect(propsOf(scopedTools, "search_memory")).toHaveProperty(
expect(propsOf(scopedTools, "addMemory")).toHaveProperty("containerTag")
expect(propsOf(scopedTools, "searchMemory")).toHaveProperty(
"containerTag",
)
expect(propsOf(scopedTools, "get_profile")).toHaveProperty("containerTag")
expect(propsOf(scopedTools, "getProfile")).toHaveProperty("containerTag")
} finally {
await scoped.close()
await plain.close()

View file

@ -44,7 +44,7 @@ describe("MCP tool analytics", () => {
}))
server.registerTool(
"search_memory",
"searchMemory",
{
inputSchema: z.object({ query: z.string(), containerTag: z.string() }),
},
@ -61,7 +61,7 @@ describe("MCP tool analytics", () => {
expect(record).toHaveBeenCalledOnce()
expect(record).toHaveBeenCalledWith(
expect.objectContaining({
toolName: "search_memory",
toolName: "searchMemory",
surface: "model_tool",
outcome: "success",
spaceExplicit: true,

View file

@ -33,14 +33,14 @@ type ClientInfoResolver = (
) => { name: string; version?: string } | null
const TOOL_SURFACES: Record<string, McpToolSurface> = {
search_memory: "model_tool",
get_profile: "model_tool",
searchMemory: "model_tool",
getProfile: "model_tool",
listDocuments: "model_tool",
getDocument: "model_tool",
listMemories: "model_tool",
listSpaces: "model_tool",
whoAmI: "model_tool",
add_memory: "model_tool",
addMemory: "model_tool",
"select-space": "app_launcher",
"memory-graph": "app_launcher",
"guided-save": "app_launcher",

View file

@ -15,7 +15,7 @@ export function register(deps: ToolDeps) {
})
deps.server.registerTool(
"add_memory",
"addMemory",
{
description:
"Add (save) or forget a memory in the user's ACTIVE space. Defaults to 'save'. The target space is the one the user selected via select-space; pass containerTag only to override it. Use 'forget' when information is outdated or the user asks to remove it.",

View file

@ -34,11 +34,11 @@ function registerProfile(client: {
}),
} as unknown as ToolDeps)
if (!handler) throw new Error("get_profile was not registered")
if (!handler) throw new Error("getProfile was not registered")
return handler
}
describe("get_profile", () => {
describe("getProfile", () => {
it("returns stable and recent profile facts for the resolved space", async () => {
const handler = registerProfile({
getProfile: async () => ({

View file

@ -11,11 +11,11 @@ export function register(deps: ToolDeps) {
})
deps.server.registerTool(
"get_profile",
"getProfile",
{
title: "Get Profile",
description:
"Get the stable and recent profile for one space — long-lived facts plus recent context. search_memory does not include this. After searching, call this if matching memories are not enough and you need who-the-user-is, preferences, or recent context. When the user names a space, resolve it with listSpaces and pass containerTag; otherwise use the active space. Use whoAmI for account identity and access, not profile facts.",
"Get the stable and recent profile for one space — long-lived facts plus recent context. searchMemory does not include this. After searching, call this if matching memories are not enough and you need who-the-user-is, preferences, or recent context. When the user names a space, resolve it with listSpaces and pass containerTag; otherwise use the active space. Use whoAmI for account identity and access, not profile facts.",
inputSchema,
outputSchema: getProfileOutputSchema,
annotations: READ_ONLY_TOOL_ANNOTATIONS,

View file

@ -11,7 +11,7 @@ export function register(deps: ToolDeps) {
{
title: "Add Memory",
description:
"Open an interactive form when the user wants to draft, review, edit, or choose the target space before saving information to Supermemory. Use this when the user wants to add a memory but has not supplied final content, or explicitly wants to review supplied content before saving. If the user provides the exact content and asks to save it immediately, use add_memory instead.",
"Open an interactive form when the user wants to draft, review, edit, or choose the target space before saving information to Supermemory. Use this when the user wants to add a memory but has not supplied final content, or explicitly wants to review supplied content before saving. If the user provides the exact content and asks to save it immediately, use addMemory instead.",
inputSchema: z.object({
prefill: z
.string()

View file

@ -33,7 +33,7 @@ export function register(deps: ToolDeps) {
{
title: "List Memories",
description:
"List the latest extracted memory entries in one space, including stable memory IDs, version information, and source document IDs. This lists memories directly, not documents. When the user names a space, resolve it with listSpaces and pass containerTag; otherwise use the active space. Use search_memory instead for semantic recall.",
"List the latest extracted memory entries in one space, including stable memory IDs, version information, and source document IDs. This lists memories directly, not documents. When the user names a space, resolve it with listSpaces and pass containerTag; otherwise use the active space. Use searchMemory instead for semantic recall.",
inputSchema,
outputSchema: listMemoriesOutputSchema,
annotations: READ_ONLY_TOOL_ANNOTATIONS,

View file

@ -46,11 +46,11 @@ function registerSearch(client: {
}),
} as unknown as ToolDeps)
if (!handler) throw new Error("search_memory was not registered")
if (!handler) throw new Error("searchMemory was not registered")
return handler
}
describe("search_memory", () => {
describe("searchMemory", () => {
it("returns matching memories without fetching or including profile", async () => {
const getProfile = vi.fn()
const handler = registerSearch({

View file

@ -18,10 +18,10 @@ export function register(deps: ToolDeps) {
})
deps.server.registerTool(
"search_memory",
"searchMemory",
{
description:
"Search memories in one space with a natural-language query. Returns matching memories only — not the space profile. If you need who-the-user-is, preferences, or recent context after searching, call get_profile. When the user names a space, resolve it with listSpaces and pass containerTag; otherwise use the active space.",
"Search memories in one space with a natural-language query. Returns matching memories only — not the space profile. If you need who-the-user-is, preferences, or recent context after searching, call getProfile. When the user names a space, resolve it with listSpaces and pass containerTag; otherwise use the active space.",
inputSchema,
outputSchema: searchMemoryOutputSchema,
annotations: READ_ONLY_TOOL_ANNOTATIONS,