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:
Mahesh Sanikommu 2026-10-07 21:02:06 -07:00
parent 1992356c5d
commit 360fc3ef91
21 changed files with 357 additions and 46 deletions

View file

@ -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,

View file

@ -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,

View 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"] })
})
})

View 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 }
}
}

View file

@ -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)

View file

@ -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))],

View file

@ -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"))],

View file

@ -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...")],

View file

@ -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,

View file

@ -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,

View file

@ -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 {

View file

@ -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(

View file

@ -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()),

View file

@ -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")],

View file

@ -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,

View file

@ -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"))],

View file

@ -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: [

View file

@ -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,

View file

@ -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...")],

View file

@ -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 } : {}),

View file

@ -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(),