mirror of
https://github.com/supermemoryai/supermemory.git
synced 2026-10-27 06:15:15 +00:00
fix(mcp): accept containerTag as a deprecated alias and emit both keys for clients with a cached tool list
Every namespace input also takes containerTag (described as deprecated), structured outputs and view messages carry both the new and the old field names, output schemas declare the legacy keys as optional so cached clients still validate, and the server sends tools/list_changed once a client finishes initializing. Compat lives in src/server/tools/compat.ts with unit tests; remove it once PostHog shows no containerTag arguments for two weeks.
This commit is contained in:
parent
1992356c5d
commit
360fc3ef91
21 changed files with 357 additions and 46 deletions
|
|
@ -99,6 +99,13 @@ export function createSupermemoryServer(
|
|||
errorResult,
|
||||
})
|
||||
|
||||
// Clients that cached the pre-v5 tool list refetch it on this notice
|
||||
server.server.oninitialized = () => {
|
||||
try {
|
||||
server.sendToolListChanged()
|
||||
} catch {}
|
||||
}
|
||||
|
||||
registerProfileResource(server, getClient, resolveSelectedNamespace)
|
||||
registerNamespacesResource(
|
||||
server,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,10 @@
|
|||
import { z } from "zod"
|
||||
import { optionalNamespaceSchema } from "../namespace"
|
||||
import {
|
||||
legacyNamespaceInput,
|
||||
namespaceArg,
|
||||
withLegacyNamespace,
|
||||
} from "./compat"
|
||||
import { MEMORY_TOOL_ANNOTATIONS } from "./annotations"
|
||||
import { addMemoryOutputSchema, type AddMemoryOutput } from "./output-schemas"
|
||||
import { textContent, type ToolDeps } from "./types"
|
||||
|
|
@ -12,6 +17,7 @@ export function register(deps: ToolDeps) {
|
|||
.describe("The memory content to save or forget"),
|
||||
action: z.enum(["save", "forget"]).optional().default("save"),
|
||||
namespace: optionalNamespaceSchema,
|
||||
...legacyNamespaceInput,
|
||||
})
|
||||
|
||||
deps.server.registerTool(
|
||||
|
|
@ -25,17 +31,17 @@ export function register(deps: ToolDeps) {
|
|||
},
|
||||
async (args) => {
|
||||
try {
|
||||
const namespace = await deps.resolveNamespace(args.namespace)
|
||||
const namespace = await deps.resolveNamespace(namespaceArg(args))
|
||||
const client = deps.getClient(namespace)
|
||||
|
||||
if (args.action === "forget") {
|
||||
const result = await client.forgetMemory(args.content)
|
||||
const structuredContent: AddMemoryOutput = {
|
||||
const structuredContent: AddMemoryOutput = withLegacyNamespace({
|
||||
action: "forget",
|
||||
success: result.success,
|
||||
namespace: result.namespace,
|
||||
message: result.message,
|
||||
}
|
||||
})
|
||||
return {
|
||||
content: [textContent(result.message)],
|
||||
structuredContent,
|
||||
|
|
@ -44,14 +50,14 @@ export function register(deps: ToolDeps) {
|
|||
|
||||
const result = await client.createMemory(args.content)
|
||||
const message = `Memory saved (ID: ${result.id}, space: ${result.namespace})`
|
||||
const structuredContent: AddMemoryOutput = {
|
||||
const structuredContent: AddMemoryOutput = withLegacyNamespace({
|
||||
action: "save",
|
||||
success: true,
|
||||
namespace: result.namespace,
|
||||
message,
|
||||
id: result.id,
|
||||
status: result.status,
|
||||
}
|
||||
})
|
||||
return {
|
||||
content: [textContent(message)],
|
||||
structuredContent,
|
||||
|
|
|
|||
168
apps/mcp/src/server/tools/compat.test.ts
Normal file
168
apps/mcp/src/server/tools/compat.test.ts
Normal file
|
|
@ -0,0 +1,168 @@
|
|||
import { describe, expect, it, vi } from "vitest"
|
||||
import { z } from "zod"
|
||||
import * as addMemory from "./add-memory"
|
||||
import { namespaceArg, withLegacyView } from "./compat"
|
||||
import * as listNamespaces from "./list-namespaces"
|
||||
import * as setActiveTag from "./set-active-tag"
|
||||
import type { ToolDeps } from "./types"
|
||||
import { errorResult } from "./types"
|
||||
|
||||
type Registered = {
|
||||
config: { inputSchema: z.ZodTypeAny; outputSchema: z.ZodTypeAny }
|
||||
cb: (
|
||||
args: Record<string, unknown>,
|
||||
context: unknown,
|
||||
) => Promise<{
|
||||
structuredContent?: Record<string, unknown>
|
||||
isError?: boolean
|
||||
}>
|
||||
}
|
||||
|
||||
function fakeDeps() {
|
||||
const tools = new Map<string, Registered>()
|
||||
const client = {
|
||||
createMemory: vi.fn().mockResolvedValue({
|
||||
id: "d1",
|
||||
status: "queued",
|
||||
namespace: "legacy_ns",
|
||||
}),
|
||||
listNamespaces: vi.fn().mockResolvedValue([
|
||||
{
|
||||
id: "1",
|
||||
namespace: "legacy_ns",
|
||||
description: null,
|
||||
documentCount: 2,
|
||||
memoryCount: 1,
|
||||
createdAt: "2026-01-01",
|
||||
updatedAt: "2026-01-02",
|
||||
},
|
||||
]),
|
||||
}
|
||||
const deps = {
|
||||
server: {
|
||||
registerTool: (name: string, config: unknown, cb: unknown) => {
|
||||
tools.set(name, { config, cb } as Registered)
|
||||
},
|
||||
},
|
||||
getClient: vi.fn(() => client),
|
||||
resolveNamespace: vi.fn(async (explicit?: string) => explicit ?? "active"),
|
||||
setActiveNamespace: vi.fn(async () => {}),
|
||||
errorResult,
|
||||
} as unknown as ToolDeps
|
||||
return { deps, tools, client }
|
||||
}
|
||||
|
||||
// Shape a client that cached the pre-v5 tool list validates against
|
||||
const legacyAddMemoryOutput = z.object({
|
||||
action: z.enum(["save", "forget"]),
|
||||
success: z.boolean(),
|
||||
containerTag: z.string(),
|
||||
message: z.string(),
|
||||
})
|
||||
|
||||
describe("containerTag compatibility", () => {
|
||||
it("prefers namespace and falls back to the deprecated containerTag", () => {
|
||||
expect(namespaceArg({ namespace: "a", containerTag: "b" })).toBe("a")
|
||||
expect(namespaceArg({ containerTag: "b" })).toBe("b")
|
||||
expect(namespaceArg({})).toBeUndefined()
|
||||
})
|
||||
|
||||
it("accepts containerTag input and ignores unknown keys", () => {
|
||||
const { deps, tools } = fakeDeps()
|
||||
addMemory.register(deps)
|
||||
const tool = tools.get("add_memory")
|
||||
if (!tool) throw new Error("add_memory not registered")
|
||||
const parsed = tool.config.inputSchema.parse({
|
||||
content: "hi",
|
||||
containerTag: "legacy_ns",
|
||||
somethingElse: 1,
|
||||
}) as { namespace?: string; containerTag?: string }
|
||||
expect(parsed.containerTag).toBe("legacy_ns")
|
||||
expect(parsed.namespace).toBeUndefined()
|
||||
})
|
||||
|
||||
it("routes a containerTag-only call to that namespace and emits both keys", async () => {
|
||||
const { deps, tools } = fakeDeps()
|
||||
addMemory.register(deps)
|
||||
const tool = tools.get("add_memory")
|
||||
if (!tool) throw new Error("add_memory not registered")
|
||||
const result = await tool.cb(
|
||||
{ content: "hi", action: "save", containerTag: "legacy_ns" },
|
||||
{},
|
||||
)
|
||||
expect(deps.getClient).toHaveBeenCalledWith("legacy_ns")
|
||||
expect(result.structuredContent).toMatchObject({
|
||||
namespace: "legacy_ns",
|
||||
containerTag: "legacy_ns",
|
||||
})
|
||||
expect(() =>
|
||||
tool.config.outputSchema.parse(result.structuredContent),
|
||||
).not.toThrow()
|
||||
expect(() =>
|
||||
legacyAddMemoryOutput.parse(result.structuredContent),
|
||||
).not.toThrow()
|
||||
})
|
||||
|
||||
it("treats containerTag as the required namespace for set-active-tag", async () => {
|
||||
const { deps, tools } = fakeDeps()
|
||||
setActiveTag.register(deps)
|
||||
const tool = tools.get("set-active-tag")
|
||||
if (!tool) throw new Error("set-active-tag not registered")
|
||||
const result = await tool.cb({ containerTag: "legacy_ns" }, {})
|
||||
expect(deps.setActiveNamespace).toHaveBeenCalledWith("legacy_ns")
|
||||
expect(result.structuredContent).toMatchObject({
|
||||
view: "confirmation",
|
||||
namespace: "legacy_ns",
|
||||
containerTag: "legacy_ns",
|
||||
})
|
||||
const missing = await tool.cb({}, {})
|
||||
expect(missing.isError).toBe(true)
|
||||
})
|
||||
|
||||
it("lists spaces with both keys per item", async () => {
|
||||
const { deps, tools } = fakeDeps()
|
||||
listNamespaces.register(deps)
|
||||
const tool = tools.get("list_spaces")
|
||||
if (!tool) throw new Error("list_spaces not registered")
|
||||
const result = await tool.cb({}, {})
|
||||
expect(result.structuredContent?.spaces).toEqual([
|
||||
expect.objectContaining({
|
||||
namespace: "legacy_ns",
|
||||
containerTag: "legacy_ns",
|
||||
}),
|
||||
])
|
||||
expect(() =>
|
||||
tool.config.outputSchema.parse(result.structuredContent),
|
||||
).not.toThrow()
|
||||
})
|
||||
|
||||
it("mirrors renamed view fields under their old names", () => {
|
||||
const picker = withLegacyView({
|
||||
view: "picker",
|
||||
namespaces: [
|
||||
{
|
||||
id: "1",
|
||||
namespace: "ns",
|
||||
documentCount: 0,
|
||||
memoryCount: 0,
|
||||
createdAt: "a",
|
||||
updatedAt: "b",
|
||||
},
|
||||
],
|
||||
activeNamespace: "ns",
|
||||
assignedNamespaces: [{ namespace: "ns", permission: "write" }],
|
||||
})
|
||||
expect(picker).toMatchObject({
|
||||
activeTag: "ns",
|
||||
containerTags: [{ namespace: "ns", containerTag: "ns" }],
|
||||
assignedTags: [{ namespace: "ns", containerTag: "ns" }],
|
||||
})
|
||||
expect(
|
||||
withLegacyView({
|
||||
view: "save",
|
||||
activeNamespace: "ns",
|
||||
writableNamespaces: ["ns"],
|
||||
}),
|
||||
).toMatchObject({ activeTag: "ns", writableTags: ["ns"] })
|
||||
})
|
||||
})
|
||||
62
apps/mcp/src/server/tools/compat.ts
Normal file
62
apps/mcp/src/server/tools/compat.ts
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
import { z } from "zod"
|
||||
import type {
|
||||
NamespaceAccess,
|
||||
NamespaceInfo,
|
||||
ViewMessage,
|
||||
} from "../../shared/types"
|
||||
import { namespaceSchema } from "../namespace"
|
||||
|
||||
// Clients that cached the pre-v5 tool list still send and expect containerTag; drop once PostHog shows no containerTag args for 2 weeks.
|
||||
export const legacyNamespaceInput = {
|
||||
containerTag: namespaceSchema
|
||||
.optional()
|
||||
.describe("Deprecated alias of namespace. Prefer namespace."),
|
||||
}
|
||||
|
||||
export const legacyNamespaceOutput = { containerTag: z.string().optional() }
|
||||
|
||||
export const namespaceArg = (args: {
|
||||
namespace?: string
|
||||
containerTag?: string
|
||||
}) => args.namespace ?? args.containerTag
|
||||
|
||||
export function withLegacyNamespace<T extends { namespace: string }>(value: T) {
|
||||
return { ...value, containerTag: value.namespace }
|
||||
}
|
||||
|
||||
export const withLegacyNamespaces = <T extends { namespace: string }>(
|
||||
values: T[],
|
||||
) => values.map(withLegacyNamespace)
|
||||
|
||||
const legacyAccess = (values: NamespaceAccess[] | null | undefined) =>
|
||||
values ? withLegacyNamespaces(values) : values
|
||||
|
||||
const legacyInfos = (values: NamespaceInfo[]) => withLegacyNamespaces(values)
|
||||
|
||||
// Mirrors every renamed view field under its pre-v5 name so cached output schemas still validate.
|
||||
export function withLegacyView(view: ViewMessage): ViewMessage {
|
||||
switch (view.view) {
|
||||
case "picker":
|
||||
return {
|
||||
...view,
|
||||
namespaces: legacyInfos(view.namespaces),
|
||||
containerTags: legacyInfos(view.namespaces),
|
||||
activeTag: view.activeNamespace,
|
||||
assignedNamespaces: legacyAccess(view.assignedNamespaces),
|
||||
assignedTags: legacyAccess(view.assignedNamespaces),
|
||||
}
|
||||
case "save":
|
||||
case "upload":
|
||||
return {
|
||||
...view,
|
||||
activeTag: view.activeNamespace,
|
||||
writableTags: view.writableNamespaces,
|
||||
}
|
||||
case "confirmation":
|
||||
case "save-success":
|
||||
case "upload-success":
|
||||
return { ...view, containerTag: view.namespace }
|
||||
case "graph":
|
||||
return { ...view, containerTag: view.namespace }
|
||||
}
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@ import { z } from "zod"
|
|||
import { documentsApiResponseSchema } from "../../shared/types"
|
||||
import { appToolMeta } from "../app-metadata"
|
||||
import { optionalNamespaceSchema } from "../namespace"
|
||||
import { legacyNamespaceInput, namespaceArg } from "./compat"
|
||||
import { READ_ONLY_TOOL_ANNOTATIONS } from "./annotations"
|
||||
import { textContent, type ToolDeps } from "./types"
|
||||
|
||||
|
|
@ -12,6 +13,7 @@ export function register(deps: ToolDeps) {
|
|||
description: "Fetch documents with memories for graph display",
|
||||
inputSchema: z.object({
|
||||
namespace: optionalNamespaceSchema,
|
||||
...legacyNamespaceInput,
|
||||
page: z.number().int().min(1).max(10_000).optional().default(1),
|
||||
limit: z.number().int().min(1).max(1_000).optional().default(200),
|
||||
}),
|
||||
|
|
@ -21,7 +23,7 @@ export function register(deps: ToolDeps) {
|
|||
},
|
||||
async (args) => {
|
||||
try {
|
||||
const namespace = await deps.resolveNamespace(args.namespace)
|
||||
const namespace = await deps.resolveNamespace(namespaceArg(args))
|
||||
const client = deps.getClient(namespace)
|
||||
const data = await client.getGraphDocuments(args.page, args.limit)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,11 @@
|
|||
import { z } from "zod"
|
||||
import { formatDocument, getDocumentContent } from "../format"
|
||||
import { optionalNamespaceSchema } from "../namespace"
|
||||
import {
|
||||
legacyNamespaceInput,
|
||||
namespaceArg,
|
||||
withLegacyNamespace,
|
||||
} from "./compat"
|
||||
import { READ_ONLY_TOOL_ANNOTATIONS } from "./annotations"
|
||||
import {
|
||||
getDocumentOutputSchema,
|
||||
|
|
@ -16,6 +21,7 @@ export function register(deps: ToolDeps) {
|
|||
.max(255, "Document ID exceeds maximum length")
|
||||
.describe("Document ID returned by list_documents or a memory result"),
|
||||
namespace: optionalNamespaceSchema,
|
||||
...legacyNamespaceInput,
|
||||
})
|
||||
|
||||
deps.server.registerTool(
|
||||
|
|
@ -30,11 +36,11 @@ export function register(deps: ToolDeps) {
|
|||
},
|
||||
async (args) => {
|
||||
try {
|
||||
const namespace = await deps.resolveNamespace(args.namespace)
|
||||
const namespace = await deps.resolveNamespace(namespaceArg(args))
|
||||
const client = deps.getClient(namespace)
|
||||
const document = await client.getDocument(args.documentId)
|
||||
const { content, truncated } = getDocumentContent(document)
|
||||
const structuredContent: GetDocumentOutput = {
|
||||
const structuredContent: GetDocumentOutput = withLegacyNamespace({
|
||||
namespace,
|
||||
document: {
|
||||
id: document.id,
|
||||
|
|
@ -47,7 +53,7 @@ export function register(deps: ToolDeps) {
|
|||
content,
|
||||
contentTruncated: truncated,
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
content: [textContent(formatDocument(document))],
|
||||
|
|
|
|||
|
|
@ -1,5 +1,10 @@
|
|||
import { z } from "zod"
|
||||
import { optionalNamespaceSchema } from "../namespace"
|
||||
import {
|
||||
legacyNamespaceInput,
|
||||
namespaceArg,
|
||||
withLegacyNamespace,
|
||||
} from "./compat"
|
||||
import { formatFactSection } from "../space-presentation"
|
||||
import { READ_ONLY_TOOL_ANNOTATIONS } from "./annotations"
|
||||
import { getProfileOutputSchema, type GetProfileOutput } from "./output-schemas"
|
||||
|
|
@ -8,6 +13,7 @@ import { textContent, type ToolDeps } from "./types"
|
|||
export function register(deps: ToolDeps) {
|
||||
const inputSchema = z.object({
|
||||
namespace: optionalNamespaceSchema,
|
||||
...legacyNamespaceInput,
|
||||
})
|
||||
|
||||
deps.server.registerTool(
|
||||
|
|
@ -22,7 +28,7 @@ export function register(deps: ToolDeps) {
|
|||
},
|
||||
async (args) => {
|
||||
try {
|
||||
const namespace = await deps.resolveNamespace(args.namespace)
|
||||
const namespace = await deps.resolveNamespace(namespaceArg(args))
|
||||
const profileResult = await deps.getClient(namespace).getProfile()
|
||||
const profile = {
|
||||
static: profileResult.profile.static,
|
||||
|
|
@ -46,10 +52,10 @@ export function register(deps: ToolDeps) {
|
|||
parts.push("No profile facts are available for this space yet.")
|
||||
}
|
||||
|
||||
const structuredContent: GetProfileOutput = {
|
||||
const structuredContent: GetProfileOutput = withLegacyNamespace({
|
||||
namespace,
|
||||
profile,
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
content: [textContent(parts.join("\n"))],
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { saveViewSchema, type ViewMessage } from "../../shared/types"
|
|||
import { appResultMeta, appToolMeta } from "../app-metadata"
|
||||
import { effectiveNamespaceAccess } from "../auth/rbac"
|
||||
import { READ_ONLY_TOOL_ANNOTATIONS } from "./annotations"
|
||||
import { withLegacyView } from "./compat"
|
||||
import { textContent, type ToolDeps } from "./types"
|
||||
|
||||
export function register(deps: ToolDeps) {
|
||||
|
|
@ -39,13 +40,13 @@ export function register(deps: ToolDeps) {
|
|||
.filter((access) => access.permission === "write")
|
||||
.map((access) => access.namespace)
|
||||
|
||||
const sc: ViewMessage = {
|
||||
const sc: ViewMessage = withLegacyView({
|
||||
view: "save",
|
||||
viewId,
|
||||
activeNamespace,
|
||||
writableNamespaces,
|
||||
prefill,
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
content: [textContent("Opening memory save form...")],
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { z } from "zod"
|
||||
import { optionalNamespaceSchema } from "../namespace"
|
||||
import { legacyNamespaceInput, namespaceArg } from "./compat"
|
||||
import { formatDocumentsList } from "../format"
|
||||
import { READ_ONLY_TOOL_ANNOTATIONS } from "./annotations"
|
||||
import {
|
||||
|
|
@ -26,6 +27,7 @@ export function register(deps: ToolDeps) {
|
|||
.default(10)
|
||||
.describe("Documents per page (default 10, max 50)"),
|
||||
namespace: optionalNamespaceSchema,
|
||||
...legacyNamespaceInput,
|
||||
})
|
||||
|
||||
deps.server.registerTool(
|
||||
|
|
@ -40,7 +42,7 @@ export function register(deps: ToolDeps) {
|
|||
},
|
||||
async (args) => {
|
||||
try {
|
||||
const namespace = await deps.resolveNamespace(args.namespace)
|
||||
const namespace = await deps.resolveNamespace(namespaceArg(args))
|
||||
const client = deps.getClient(namespace)
|
||||
const data = await client.listDocuments(
|
||||
args.page ?? 1,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { z } from "zod"
|
||||
import { optionalNamespaceSchema } from "../namespace"
|
||||
import { legacyNamespaceInput, namespaceArg } from "./compat"
|
||||
import { formatMemoryEntriesList } from "../format"
|
||||
import { READ_ONLY_TOOL_ANNOTATIONS } from "./annotations"
|
||||
import {
|
||||
|
|
@ -26,6 +27,7 @@ export function register(deps: ToolDeps) {
|
|||
.default(10)
|
||||
.describe("Memory entries per page (default 10, max 50)"),
|
||||
namespace: optionalNamespaceSchema,
|
||||
...legacyNamespaceInput,
|
||||
})
|
||||
|
||||
deps.server.registerTool(
|
||||
|
|
@ -40,7 +42,7 @@ export function register(deps: ToolDeps) {
|
|||
},
|
||||
async (args) => {
|
||||
try {
|
||||
const namespace = await deps.resolveNamespace(args.namespace)
|
||||
const namespace = await deps.resolveNamespace(namespaceArg(args))
|
||||
const client = deps.getClient(namespace)
|
||||
const data = await client.listMemoryEntries(
|
||||
args.page ?? 1,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { z } from "zod"
|
||||
import { listSpacesOutputSchema } from "../../shared/types"
|
||||
import { READ_ONLY_TOOL_ANNOTATIONS } from "./annotations"
|
||||
import { withLegacyNamespaces } from "./compat"
|
||||
import { textContent, type ToolDeps } from "./types"
|
||||
|
||||
export function register(deps: ToolDeps) {
|
||||
|
|
@ -16,13 +17,15 @@ export function register(deps: ToolDeps) {
|
|||
async () => {
|
||||
try {
|
||||
const namespaces = await deps.getClient().listNamespaces()
|
||||
const spaces = namespaces.map((entry) => ({
|
||||
namespace: entry.namespace,
|
||||
description: entry.description,
|
||||
documentCount: entry.documentCount,
|
||||
memoryCount: entry.memoryCount,
|
||||
updatedAt: entry.updatedAt,
|
||||
}))
|
||||
const spaces = withLegacyNamespaces(
|
||||
namespaces.map((entry) => ({
|
||||
namespace: entry.namespace,
|
||||
description: entry.description,
|
||||
documentCount: entry.documentCount,
|
||||
memoryCount: entry.memoryCount,
|
||||
updatedAt: entry.updatedAt,
|
||||
})),
|
||||
)
|
||||
|
||||
if (spaces.length === 0) {
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -2,12 +2,14 @@ import { z } from "zod"
|
|||
import { graphViewSchema, type ViewMessage } from "../../shared/types"
|
||||
import { appResultMeta, appToolMeta } from "../app-metadata"
|
||||
import { optionalNamespaceSchema } from "../namespace"
|
||||
import { legacyNamespaceInput, namespaceArg, withLegacyView } from "./compat"
|
||||
import { READ_ONLY_TOOL_ANNOTATIONS } from "./annotations"
|
||||
import { textContent, type ToolDeps } from "./types"
|
||||
|
||||
export function register(deps: ToolDeps) {
|
||||
const inputSchema = z.object({
|
||||
namespace: optionalNamespaceSchema,
|
||||
...legacyNamespaceInput,
|
||||
})
|
||||
|
||||
deps.server.registerTool(
|
||||
|
|
@ -24,14 +26,14 @@ export function register(deps: ToolDeps) {
|
|||
async (args) => {
|
||||
try {
|
||||
const viewId = crypto.randomUUID()
|
||||
const namespace = await deps.resolveNamespace(args.namespace)
|
||||
const namespace = await deps.resolveNamespace(namespaceArg(args))
|
||||
const client = deps.getClient(namespace)
|
||||
const result = await client.getGraphDocuments(1, 200)
|
||||
const memoryCount = result.documents.reduce(
|
||||
(sum, document) => sum + document.memoryEntries.length,
|
||||
0,
|
||||
)
|
||||
const sc: ViewMessage = {
|
||||
const sc: ViewMessage = withLegacyView({
|
||||
view: "graph",
|
||||
viewId,
|
||||
namespace,
|
||||
|
|
@ -42,7 +44,7 @@ export function register(deps: ToolDeps) {
|
|||
totalDocumentCount: result.pagination.totalItems,
|
||||
truncated: result.documents.length < result.pagination.totalItems,
|
||||
rendered: true,
|
||||
}
|
||||
})
|
||||
return {
|
||||
content: [
|
||||
textContent(
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { z } from "zod"
|
||||
import { legacyNamespaceOutput } from "./compat"
|
||||
import {
|
||||
namespaceAccessSchema,
|
||||
memoriesListSchema,
|
||||
|
|
@ -20,6 +21,7 @@ export const addMemoryOutputSchema = z.object({
|
|||
action: z.enum(["save", "forget"]),
|
||||
success: z.boolean(),
|
||||
namespace: z.string(),
|
||||
...legacyNamespaceOutput,
|
||||
message: z.string(),
|
||||
id: z.string().optional(),
|
||||
status: z.string().optional(),
|
||||
|
|
@ -29,6 +31,7 @@ export type AddMemoryOutput = z.infer<typeof addMemoryOutputSchema>
|
|||
|
||||
export const getDocumentOutputSchema = z.object({
|
||||
namespace: z.string(),
|
||||
...legacyNamespaceOutput,
|
||||
document: z.object({
|
||||
id: z.string(),
|
||||
title: z.string().nullable(),
|
||||
|
|
@ -58,6 +61,7 @@ export type ListMemoriesOutput = z.infer<typeof listMemoriesOutputSchema>
|
|||
export const searchMemoryOutputSchema = z.object({
|
||||
query: z.string(),
|
||||
namespace: z.string(),
|
||||
...legacyNamespaceOutput,
|
||||
profile: z
|
||||
.object({
|
||||
static: z.array(z.string()),
|
||||
|
|
@ -80,6 +84,7 @@ export type SearchMemoryOutput = z.infer<typeof searchMemoryOutputSchema>
|
|||
|
||||
export const getProfileOutputSchema = z.object({
|
||||
namespace: z.string(),
|
||||
...legacyNamespaceOutput,
|
||||
profile: z.object({
|
||||
static: z.array(z.string()),
|
||||
dynamic: z.array(z.string()),
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { z } from "zod"
|
|||
import { uploadPreparationSchema } from "../../shared/types"
|
||||
import { appToolMeta } from "../app-metadata"
|
||||
import { namespaceSchema } from "../namespace"
|
||||
import { legacyNamespaceInput, namespaceArg } from "./compat"
|
||||
import { ADDITIVE_MEMORY_TOOL_ANNOTATIONS } from "./annotations"
|
||||
import { textContent, type ToolDeps } from "./types"
|
||||
|
||||
|
|
@ -10,14 +11,20 @@ export function register(deps: ToolDeps) {
|
|||
"prepare-file-upload",
|
||||
{
|
||||
description: "Prepare a direct file upload",
|
||||
inputSchema: z.object({ namespace: namespaceSchema }),
|
||||
inputSchema: z.object({
|
||||
namespace: namespaceSchema.optional(),
|
||||
...legacyNamespaceInput,
|
||||
}),
|
||||
outputSchema: uploadPreparationSchema,
|
||||
annotations: ADDITIVE_MEMORY_TOOL_ANNOTATIONS,
|
||||
_meta: appToolMeta(["app"]),
|
||||
},
|
||||
async (args) => {
|
||||
try {
|
||||
const preparation = await deps.createUploadSession(args.namespace)
|
||||
const namespace = namespaceArg(args)
|
||||
if (!namespace)
|
||||
return deps.errorResult(new Error("namespace is required"))
|
||||
const preparation = await deps.createUploadSession(namespace)
|
||||
|
||||
return {
|
||||
content: [textContent("Upload session prepared")],
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { z } from "zod"
|
|||
import { saveSuccessViewSchema, type ViewMessage } from "../../shared/types"
|
||||
import { appResultMeta, appToolMeta } from "../app-metadata"
|
||||
import { namespaceSchema } from "../namespace"
|
||||
import { legacyNamespaceInput, namespaceArg, withLegacyView } from "./compat"
|
||||
import { ADDITIVE_MEMORY_TOOL_ANNOTATIONS } from "./annotations"
|
||||
import { textContent, type ToolDeps } from "./types"
|
||||
|
||||
|
|
@ -12,7 +13,8 @@ export function register(deps: ToolDeps) {
|
|||
description: "Save content to memory",
|
||||
inputSchema: z.object({
|
||||
content: z.string().min(1),
|
||||
namespace: namespaceSchema,
|
||||
namespace: namespaceSchema.optional(),
|
||||
...legacyNamespaceInput,
|
||||
viewId: z.string().uuid().optional(),
|
||||
}),
|
||||
outputSchema: saveSuccessViewSchema,
|
||||
|
|
@ -21,15 +23,18 @@ export function register(deps: ToolDeps) {
|
|||
},
|
||||
async (args) => {
|
||||
try {
|
||||
const namespace = namespaceArg(args)
|
||||
if (!namespace)
|
||||
return deps.errorResult(new Error("namespace is required"))
|
||||
const viewId = args.viewId ?? crypto.randomUUID()
|
||||
const client = deps.getClient(args.namespace)
|
||||
const client = deps.getClient(namespace)
|
||||
const result = await client.createMemory(args.content)
|
||||
const sc: ViewMessage = {
|
||||
const sc: ViewMessage = withLegacyView({
|
||||
view: "save-success",
|
||||
viewId,
|
||||
id: result.id,
|
||||
namespace: args.namespace,
|
||||
}
|
||||
namespace,
|
||||
})
|
||||
return {
|
||||
content: [textContent(`Memory saved: ${result.id}`)],
|
||||
structuredContent: sc,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,11 @@
|
|||
import { z } from "zod"
|
||||
import { getMemoryText } from "../client"
|
||||
import { optionalNamespaceSchema } from "../namespace"
|
||||
import {
|
||||
legacyNamespaceInput,
|
||||
namespaceArg,
|
||||
withLegacyNamespace,
|
||||
} from "./compat"
|
||||
import { READ_ONLY_TOOL_ANNOTATIONS } from "./annotations"
|
||||
import {
|
||||
searchMemoryOutputSchema,
|
||||
|
|
@ -15,6 +20,7 @@ export function register(deps: ToolDeps) {
|
|||
.max(1000, "Query exceeds maximum length")
|
||||
.describe("The search query to find relevant memories"),
|
||||
namespace: optionalNamespaceSchema,
|
||||
...legacyNamespaceInput,
|
||||
})
|
||||
|
||||
deps.server.registerTool(
|
||||
|
|
@ -28,7 +34,7 @@ export function register(deps: ToolDeps) {
|
|||
},
|
||||
async (args) => {
|
||||
try {
|
||||
const namespace = await deps.resolveNamespace(args.namespace)
|
||||
const namespace = await deps.resolveNamespace(namespaceArg(args))
|
||||
const client = deps.getClient(namespace)
|
||||
|
||||
const searchResult = await client.search(args.query)
|
||||
|
|
@ -50,13 +56,13 @@ export function register(deps: ToolDeps) {
|
|||
parts.push("No matching memories found.")
|
||||
}
|
||||
|
||||
const structuredContent: SearchMemoryOutput = {
|
||||
const structuredContent: SearchMemoryOutput = withLegacyNamespace({
|
||||
query: args.query,
|
||||
namespace,
|
||||
results,
|
||||
total: searchResult.total,
|
||||
timing: searchResult.timing,
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
content: [textContent(parts.join("\n"))],
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { pickerViewSchema, type ViewMessage } from "../../shared/types"
|
|||
import { appResultMeta, appToolMeta } from "../app-metadata"
|
||||
import { effectiveNamespaceAccess } from "../auth/rbac"
|
||||
import { READ_ONLY_TOOL_ANNOTATIONS } from "./annotations"
|
||||
import { withLegacyView } from "./compat"
|
||||
import { textContent, type ToolDeps } from "./types"
|
||||
|
||||
export function register(deps: ToolDeps) {
|
||||
|
|
@ -31,13 +32,13 @@ export function register(deps: ToolDeps) {
|
|||
session,
|
||||
)
|
||||
|
||||
const sc: ViewMessage = {
|
||||
const sc: ViewMessage = withLegacyView({
|
||||
view: "picker",
|
||||
viewId,
|
||||
namespaces,
|
||||
activeNamespace,
|
||||
assignedNamespaces,
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
content: [
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { z } from "zod"
|
|||
import { confirmationViewSchema, type ViewMessage } from "../../shared/types"
|
||||
import { appResultMeta, appToolMeta } from "../app-metadata"
|
||||
import { namespaceSchema } from "../namespace"
|
||||
import { legacyNamespaceInput, namespaceArg, withLegacyView } from "./compat"
|
||||
import { SETTINGS_TOOL_ANNOTATIONS } from "./annotations"
|
||||
import { textContent, type ToolDeps } from "./types"
|
||||
|
||||
|
|
@ -11,7 +12,8 @@ export function register(deps: ToolDeps) {
|
|||
{
|
||||
description: "Set the active Supermemory space for this account",
|
||||
inputSchema: z.object({
|
||||
namespace: namespaceSchema,
|
||||
namespace: namespaceSchema.optional(),
|
||||
...legacyNamespaceInput,
|
||||
viewId: z.string().uuid().optional(),
|
||||
}),
|
||||
outputSchema: confirmationViewSchema,
|
||||
|
|
@ -19,7 +21,9 @@ export function register(deps: ToolDeps) {
|
|||
annotations: SETTINGS_TOOL_ANNOTATIONS,
|
||||
},
|
||||
async (args) => {
|
||||
const { namespace } = args
|
||||
const namespace = namespaceArg(args)
|
||||
if (!namespace)
|
||||
return deps.errorResult(new Error("namespace is required"))
|
||||
try {
|
||||
const viewId = args.viewId ?? crypto.randomUUID()
|
||||
const namespaces = await deps.getClient().listNamespaces()
|
||||
|
|
@ -29,11 +33,11 @@ export function register(deps: ToolDeps) {
|
|||
)
|
||||
}
|
||||
await deps.setActiveNamespace(namespace)
|
||||
const sc: ViewMessage = {
|
||||
const sc: ViewMessage = withLegacyView({
|
||||
view: "confirmation",
|
||||
viewId,
|
||||
namespace,
|
||||
}
|
||||
})
|
||||
return {
|
||||
content: [textContent(`Active space set to ${namespace}`)],
|
||||
structuredContent: sc,
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { uploadViewSchema, type ViewMessage } from "../../shared/types"
|
|||
import { appResultMeta, appToolMeta } from "../app-metadata"
|
||||
import { effectiveNamespaceAccess } from "../auth/rbac"
|
||||
import { READ_ONLY_TOOL_ANNOTATIONS } from "./annotations"
|
||||
import { withLegacyView } from "./compat"
|
||||
import { textContent, type ToolDeps } from "./types"
|
||||
|
||||
export function register(deps: ToolDeps) {
|
||||
|
|
@ -32,12 +33,12 @@ export function register(deps: ToolDeps) {
|
|||
.filter((access) => access.permission === "write")
|
||||
.map((access) => access.namespace)
|
||||
|
||||
const sc: ViewMessage = {
|
||||
const sc: ViewMessage = withLegacyView({
|
||||
view: "upload",
|
||||
viewId,
|
||||
activeNamespace,
|
||||
writableNamespaces,
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
content: [textContent("Opening file upload form...")],
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { z } from "zod"
|
||||
import { READ_ONLY_TOOL_ANNOTATIONS } from "./annotations"
|
||||
import { withLegacyNamespaces } from "./compat"
|
||||
import { whoAmIOutputSchema, type WhoAmIOutput } from "./output-schemas"
|
||||
import { textContent, type ToolDeps } from "./types"
|
||||
|
||||
|
|
@ -28,8 +29,8 @@ export function register(deps: ToolDeps) {
|
|||
accessType: session.accessType ?? "full",
|
||||
activeSpace: activeNamespace ?? null,
|
||||
assignedSpaces:
|
||||
session.accessType === "restricted"
|
||||
? (session.namespaces ?? null)
|
||||
session.accessType === "restricted" && session.namespaces
|
||||
? withLegacyNamespaces(session.namespaces)
|
||||
: null,
|
||||
...(session.scope ? { scope: session.scope } : {}),
|
||||
...(client ? { client } : {}),
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import { z } from "zod"
|
|||
|
||||
export const namespaceAccessSchema = z.object({
|
||||
namespace: z.string(),
|
||||
containerTag: z.string().optional(),
|
||||
permission: z.enum(["read", "write"]),
|
||||
})
|
||||
|
||||
|
|
@ -57,6 +58,7 @@ export type SessionInfo = z.output<typeof sessionInfoSchema>
|
|||
export const namespaceInfoSchema = z.object({
|
||||
id: z.string(),
|
||||
namespace: z.string(),
|
||||
containerTag: z.string().optional(),
|
||||
description: z.string().nullish(),
|
||||
documentCount: z.number().int().nonnegative(),
|
||||
memoryCount: z.number().int().nonnegative(),
|
||||
|
|
@ -68,6 +70,7 @@ export type NamespaceInfo = z.infer<typeof namespaceInfoSchema>
|
|||
|
||||
export const spaceSummarySchema = z.object({
|
||||
namespace: z.string(),
|
||||
containerTag: z.string().optional(),
|
||||
description: z.string().nullish(),
|
||||
documentCount: z.number().int().nonnegative(),
|
||||
memoryCount: z.number().int().nonnegative(),
|
||||
|
|
@ -176,12 +179,16 @@ export const pickerViewSchema = z.object({
|
|||
namespaces: z.array(namespaceInfoSchema),
|
||||
activeNamespace: z.string().nullish(),
|
||||
assignedNamespaces: z.array(namespaceAccessSchema).nullable().optional(),
|
||||
containerTags: z.array(namespaceInfoSchema).optional(),
|
||||
activeTag: z.string().nullish(),
|
||||
assignedTags: z.array(namespaceAccessSchema).nullable().optional(),
|
||||
})
|
||||
|
||||
export const confirmationViewSchema = z.object({
|
||||
view: z.literal("confirmation"),
|
||||
viewId: viewIdSchema,
|
||||
namespace: z.string(),
|
||||
containerTag: z.string().optional(),
|
||||
})
|
||||
|
||||
export const saveViewSchema = z.object({
|
||||
|
|
@ -190,6 +197,8 @@ export const saveViewSchema = z.object({
|
|||
activeNamespace: z.string().nullish(),
|
||||
writableNamespaces: z.array(z.string()),
|
||||
prefill: z.string().optional(),
|
||||
activeTag: z.string().nullish(),
|
||||
writableTags: z.array(z.string()).optional(),
|
||||
})
|
||||
|
||||
export const saveSuccessViewSchema = z.object({
|
||||
|
|
@ -197,6 +206,7 @@ export const saveSuccessViewSchema = z.object({
|
|||
viewId: viewIdSchema,
|
||||
id: z.string(),
|
||||
namespace: z.string(),
|
||||
containerTag: z.string().optional(),
|
||||
})
|
||||
|
||||
export const uploadViewSchema = z.object({
|
||||
|
|
@ -204,6 +214,8 @@ export const uploadViewSchema = z.object({
|
|||
viewId: viewIdSchema,
|
||||
activeNamespace: z.string().nullish(),
|
||||
writableNamespaces: z.array(z.string()),
|
||||
activeTag: z.string().nullish(),
|
||||
writableTags: z.array(z.string()).optional(),
|
||||
})
|
||||
|
||||
export const uploadSuccessViewSchema = z.object({
|
||||
|
|
@ -212,6 +224,7 @@ export const uploadSuccessViewSchema = z.object({
|
|||
id: z.string(),
|
||||
fileName: z.string(),
|
||||
namespace: z.string(),
|
||||
containerTag: z.string().optional(),
|
||||
})
|
||||
|
||||
export const uploadPreparationSchema = z.object({
|
||||
|
|
@ -229,6 +242,7 @@ export const graphViewSchema = z.object({
|
|||
view: z.literal("graph"),
|
||||
viewId: viewIdSchema,
|
||||
namespace: z.string().optional(),
|
||||
containerTag: z.string().optional(),
|
||||
documents: z.array(documentWithMemoriesSchema),
|
||||
totalCount: z.number().int().nonnegative(),
|
||||
documentCount: z.number().int().nonnegative(),
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue