fix(mcp): share the accessible default with prompts and resources, prefer writable spaces

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
This commit is contained in:
Dhravya 2026-10-16 06:02:28 +00:00 • committed by Dhravya Shah
parent 5381882fb6
commit d40b2f5a07
6 changed files with 52 additions and 12 deletions

View file

@ -1,5 +1,5 @@
import type { McpServer } from "@modelcontextprotocol/server"
import { DEFAULT_NAMESPACE, type SupermemoryClient } from "../client"
import type { SupermemoryClient } from "../client"
import {
compactDescription,
formatFactSection,
@ -15,6 +15,7 @@ export function registerContextPrompt(
server: McpServer,
getClient: (namespace?: string) => SupermemoryClient,
resolveNamespace: () => Promise<string | undefined>,
getDefaultNamespace: () => Promise<string>,
) {
server.registerPrompt(
"context",
@ -24,7 +25,7 @@ export function registerContextPrompt(
async () => {
try {
const selectedNamespace = await resolveNamespace()
const activeKey = selectedNamespace ?? DEFAULT_NAMESPACE
const activeKey = selectedNamespace ?? (await getDefaultNamespace())
const [profileResult, spaces] = await Promise.all([
getClient(activeKey).getProfile(),
getClient().listNamespaces(),

View file

@ -1,11 +1,12 @@
import type { McpServer } from "@modelcontextprotocol/server"
import { DEFAULT_NAMESPACE, type SupermemoryClient } from "../client"
import type { SupermemoryClient } from "../client"
import { formatSpaceRow, sortSpaces } from "../space-presentation"
export function registerNamespacesResource(
server: McpServer,
getClient: () => SupermemoryClient,
resolveNamespace: () => Promise<string | undefined>,
getDefaultNamespace: () => Promise<string>,
) {
server.registerResource("My Spaces", "supermemory://spaces", {}, async () => {
const client = getClient()
@ -13,7 +14,7 @@ export function registerNamespacesResource(
client.listNamespaces(),
resolveNamespace(),
])
const activeKey = selectedNamespace ?? DEFAULT_NAMESPACE
const activeKey = selectedNamespace ?? (await getDefaultNamespace())
const rows = sortSpaces(namespaces, activeKey).map((space) =>
formatSpaceRow(space, activeKey),
)

View file

@ -1,5 +1,5 @@
import type { McpServer } from "@modelcontextprotocol/server"
import { DEFAULT_NAMESPACE, type SupermemoryClient } from "../client"
import type { SupermemoryClient } from "../client"
import {
compactDescription,
formatFactSection,
@ -12,6 +12,7 @@ export function registerProfileResource(
server: McpServer,
getClient: (namespace?: string) => SupermemoryClient,
resolveNamespace: () => Promise<string | undefined>,
getDefaultNamespace: () => Promise<string>,
) {
server.registerResource(
"Active Space Profile",
@ -19,7 +20,7 @@ export function registerProfileResource(
{},
async () => {
const selectedNamespace = await resolveNamespace()
const activeKey = selectedNamespace ?? DEFAULT_NAMESPACE
const activeKey = selectedNamespace ?? (await getDefaultNamespace())
const [profileResult, spaces] = await Promise.all([
getClient(activeKey).getProfile(),
getClient().listNamespaces(),

View file

@ -98,9 +98,10 @@ export function createSupermemoryServer(
spaceState.setActiveNamespace(namespace)
const resolveSelectedNamespace = (explicit?: string) =>
resolveSpaceNamespace(explicit, getActiveNamespace)
const getDefaultNamespace = () =>
defaultNamespaceFor(actor.bearerToken, apiUrl)
const resolveNamespace = async (explicit?: string) =>
(await resolveSelectedNamespace(explicit)) ??
(await defaultNamespaceFor(actor.bearerToken, apiUrl))
(await resolveSelectedNamespace(explicit)) ?? (await getDefaultNamespace())
const createUploadSession = async (namespace: string) => {
const uploadId = crypto.randomUUID()
const uploadToken = [crypto.randomUUID(), crypto.randomUUID()]
@ -139,14 +140,25 @@ export function createSupermemoryServer(
} catch {}
}
registerProfileResource(server, getClient, resolveSelectedNamespace)
registerProfileResource(
server,
getClient,
resolveSelectedNamespace,
getDefaultNamespace,
)
registerNamespacesResource(
server,
() => getClient(),
resolveSelectedNamespace,
getDefaultNamespace,
)
registerWidgetResource(server, mcpOrigin)
registerContextPrompt(server, getClient, resolveSelectedNamespace)
registerContextPrompt(
server,
getClient,
resolveSelectedNamespace,
getDefaultNamespace,
)
return server
}

View file

@ -80,6 +80,23 @@ describe("space application state", () => {
scope: { type: "scoped", tags: ["notes"] },
}),
).toBe("notes")
expect(
accessibleDefaultNamespace({
user,
accessType: "restricted",
namespaces: [
{ namespace: "readonly", permission: "read" },
{ namespace: "team", permission: "write" },
],
}),
).toBe("team")
expect(
accessibleDefaultNamespace({
user,
accessType: "restricted",
namespaces: [{ namespace: "readonly", permission: "read" }],
}),
).toBe("readonly")
expect(
accessibleDefaultNamespace({
user,

View file

@ -15,7 +15,7 @@ export async function resolveNamespace(
return explicit ?? (await getActiveNamespace())
}
// Mirrors the API's hard allowlist: scoped keys and restricted members can only reach their listed spaces.
// Mirrors the API's hard allowlist (scoped keys, restricted members), preferring a space writes can reach.
export function accessibleDefaultNamespace(session: SessionInfo): string {
const keyTags =
session.scope?.type === "scoped"
@ -32,5 +32,13 @@ export function accessibleDefaultNamespace(session: SessionInfo): string {
if (!allowed?.[0] || allowed.includes(DEFAULT_NAMESPACE)) {
return DEFAULT_NAMESPACE
}
return allowed[0]
const writable =
session.scope?.permission === "read"
? undefined
: allowed.find(
(tag) =>
session.namespaces?.find((access) => access.namespace === tag)
?.permission !== "read",
)
return writable ?? allowed[0]
}