From 722b1e8053e2fba58d7c7dcabbe9d7b6475286d4 Mon Sep 17 00:00:00 2001 From: Mahesh Sanikommu Date: Thu, 8 Oct 2026 10:32:49 -0700 Subject: [PATCH] fix(mcp): one namespace call where one is needed; upload prep falls back to the active space listNamespaces fetches a single page instead of up to 20; set_active_tag checks access with namespaces.get (404 means no access) so namespaces past the first page work. prepare-file-upload resolves the namespace like every other tool, so an old cached widget that sends no namespace still uploads. --- apps/mcp/src/server/client/index.ts | 62 ++++++++++++------- apps/mcp/src/server/tools/compat.test.ts | 10 +++ .../src/server/tools/prepare-file-upload.ts | 4 +- apps/mcp/src/server/tools/set-active-tag.ts | 3 +- 4 files changed, 51 insertions(+), 28 deletions(-) diff --git a/apps/mcp/src/server/client/index.ts b/apps/mcp/src/server/client/index.ts index c903da6d..39256cf7 100644 --- a/apps/mcp/src/server/client/index.ts +++ b/apps/mcp/src/server/client/index.ts @@ -22,7 +22,6 @@ export const DEFAULT_NAMESPACE = "sm_project_default" const FETCH_TIMEOUT_MS = 30_000 const MCP_SOURCE = "supermemory-mcp" const NAMESPACE_PAGE_LIMIT = 100 -const MAX_NAMESPACE_PAGES = 20 // v4 defaults; v5 lowered the threshold to 0.3. const SEARCH_THRESHOLD = 0.6 // Analytics matches on this text to tag quota errors, keep them in sync. @@ -154,6 +153,18 @@ function extractApiErrorMessage(raw: unknown): string | undefined { } } +export interface NamespaceSettings { + namespace: string + supportingContext: string | null + createdAt: string + updatedAt: string +} + +const isNotFound = (error: unknown): boolean => { + const e = error as { statusCode?: number; status?: number } + return e?.statusCode === 404 || e?.status === 404 +} + export class SupermemoryClient { private client: Supermemory private namespace: string @@ -273,30 +284,35 @@ export class SupermemoryClient { } } - async listNamespaces(): Promise { + // v5 get returns settings only (no counts); null means missing or no access + async getNamespace(namespace: string): Promise { try { - const namespaces: NamespaceInfo[] = [] - for (let page = 1; page <= MAX_NAMESPACE_PAGES; page++) { - const result = await this.client.namespaces.list({ - page, - limit: NAMESPACE_PAGE_LIMIT, - }) - for (const entry of result.namespaces) { - namespaces.push({ - id: entry.id, - namespace: entry.namespace, - description: entry.description, - documentCount: entry.documentCount, - memoryCount: entry.memoryCount, - createdAt: entry.system.createdAt, - updatedAt: entry.system.updatedAt, - }) - } - if (result.pagination.currentPage >= result.pagination.totalPages) { - break - } + const entry = await this.client.namespaces.get(namespace) + return { + namespace: entry.namespace, + supportingContext: entry.supportingContext, + createdAt: entry.system.createdAt, + updatedAt: entry.system.updatedAt, } - return namespaces + } catch (error) { + if (isNotFound(error)) return null + this.handleError(error) + } + } + + // One page is enough for pickers; single-namespace reads use getNamespace + async listNamespaces(limit = NAMESPACE_PAGE_LIMIT): Promise { + try { + const result = await this.client.namespaces.list({ page: 1, limit }) + return result.namespaces.map((entry) => ({ + id: entry.id, + namespace: entry.namespace, + description: entry.description, + documentCount: entry.documentCount, + memoryCount: entry.memoryCount, + createdAt: entry.system.createdAt, + updatedAt: entry.system.updatedAt, + })) } catch (error) { this.handleError(error) } diff --git a/apps/mcp/src/server/tools/compat.test.ts b/apps/mcp/src/server/tools/compat.test.ts index bdaa1aee..5eba1d1a 100644 --- a/apps/mcp/src/server/tools/compat.test.ts +++ b/apps/mcp/src/server/tools/compat.test.ts @@ -26,6 +26,16 @@ function fakeDeps() { status: "queued", namespace: "legacy_ns", }), + getNamespace: vi.fn().mockImplementation(async (ns: string) => + ns === "legacy_ns" + ? { + namespace: ns, + supportingContext: null, + createdAt: "2026-01-01", + updatedAt: "2026-01-02", + } + : null, + ), listNamespaces: vi.fn().mockResolvedValue([ { id: "1", diff --git a/apps/mcp/src/server/tools/prepare-file-upload.ts b/apps/mcp/src/server/tools/prepare-file-upload.ts index d01e415b..9b003cc5 100644 --- a/apps/mcp/src/server/tools/prepare-file-upload.ts +++ b/apps/mcp/src/server/tools/prepare-file-upload.ts @@ -21,9 +21,7 @@ export function register(deps: ToolDeps) { }, async (args) => { try { - const namespace = namespaceArg(args) - if (!namespace) - return deps.errorResult(new Error("namespace is required")) + const namespace = await deps.resolveNamespace(namespaceArg(args)) const preparation = await deps.createUploadSession(namespace) return { diff --git a/apps/mcp/src/server/tools/set-active-tag.ts b/apps/mcp/src/server/tools/set-active-tag.ts index 1b1fb729..b60f8b6a 100644 --- a/apps/mcp/src/server/tools/set-active-tag.ts +++ b/apps/mcp/src/server/tools/set-active-tag.ts @@ -26,8 +26,7 @@ export function register(deps: ToolDeps) { return deps.errorResult(new Error("namespace is required")) try { const viewId = args.viewId ?? crypto.randomUUID() - const namespaces = await deps.getClient().listNamespaces() - if (!namespaces.some((entry) => entry.namespace === namespace)) { + if (!(await deps.getClient().getNamespace(namespace))) { return deps.errorResult( new Error(`No access to namespace '${namespace}'.`), )