use space across mcp surfaces

This commit is contained in:
Prasanna A P 2026-07-29 21:53:03 -07:00
parent d6e34d2131
commit 8a9fcd52dc
34 changed files with 170 additions and 162 deletions

View file

@ -1,7 +1,7 @@
# Supermemory MCP Server
The Supermemory MCP server gives authenticated AI clients access to a user's
memories, profile, workspaces, and interactive MCP Apps.
memories, profile, spaces, and interactive MCP Apps.
## Runtime Model
@ -9,17 +9,17 @@ memories, profile, workspaces, and interactive MCP Apps.
- Modern MCP `2026-07-28` plus stateless compatibility for 2025 clients
- OAuth token validation on every request
- No MCP protocol session or protocol Durable Object
- Active workspace stored as application state in a dedicated Durable Object
- Workspace state keyed by authenticated `organizationId + userId`
- Active space stored as application state in a dedicated Durable Object
- Space state keyed by authenticated `organizationId + userId`
The workspace used by an operation resolves in this order:
The space used by an operation resolves in this order:
1. An explicit `containerTag` tool or prompt argument
2. The account's durable active workspace
2. The account's durable active space
3. The Supermemory client default, `sm_project_default`
An explicit override applies only to that call. It does not mutate the active
workspace.
space.
## Server URL
@ -49,18 +49,18 @@ The client discovers the OAuth authorization server through
| Tool | Purpose |
| --- | --- |
| `search_memory` | Search memories and optionally include profile context |
| `listDocuments` | List document metadata and summaries in a workspace |
| `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 workspaces visible to the authenticated account |
| `whoAmI` | Return identity, access, and active-workspace context |
| `listSpaces` | List spaces visible to the authenticated account |
| `whoAmI` | Return identity, access, and active-space context |
| `add_memory` | Save or forget a memory |
### MCP App launchers
| Tool | Purpose |
| --- | --- |
| `select-workspace` | Open the interactive workspace picker |
| `select-space` | Open the interactive space picker |
| `memory-graph` | Open the interactive memory graph |
| `guided-save` | Open the guided memory form |
| `upload-file` | Open the file upload form |
@ -71,7 +71,7 @@ These tools are available to the embedded MCP App and hidden from the model.
| Tool | Purpose |
| --- | --- |
| `set-active-tag` | Persist the selected active workspace |
| `set-active-tag` | Persist the selected active space |
| `save-memory` | Submit the guided save form |
| `upload-file-submit` | Submit an encoded file upload |
| `fetch-graph-data` | Fetch graph documents for the app |
@ -80,10 +80,10 @@ These tools are available to the embedded MCP App and hidden from the model.
| Kind | Name or URI | Purpose |
| --- | --- | --- |
| Resource | `supermemory://profile` | Profile facts in the effective workspace |
| Resource | `supermemory://container-tags` | Visible workspaces |
| Resource | `supermemory://profile` | Profile facts in the effective space |
| Resource | `supermemory://spaces` | Visible spaces |
| Resource | `ui://supermemory/app-v3.html` | Embedded MCP App bundle |
| Prompt | `context` | Profile and recent context for an optional workspace |
| Prompt | `context` | Profile and recent context for an optional space |
The App resource and tool metadata include both current nested `ui` metadata and
the legacy flat resource URI key while MCP Apps completes its SDK v2 migration.
@ -131,10 +131,12 @@ discovery and rejection tests still run.
| `API_URL` | Supermemory API and OAuth issuer | `https://api.supermemory.ai` |
| `MCP_RESOURCE` | Expected OAuth audience | `https://mcp.supermemory.ai/mcp` |
| `ALLOWED_MCP_ORIGIN_HOSTNAMES` | Additional comma-separated browser origins | Built-in host allowlist |
| `POSTHOG_API_KEY` | Server-side MCP tool analytics project key | Disabled |
| `POSTHOG_HOST` | PostHog ingestion host | `https://us.i.posthog.com` |
## Storage And Rollout
`WorkspaceState` stores only the active container tag. It never stores bearer
`WorkspaceState` stores only the active space's container tag. It never stores bearer
tokens, MCP client identity, or protocol messages.
The old `SupermemoryMCP` class and binding remain inert for one rollout. This

View file

@ -18,7 +18,7 @@ const EXPECTED_TOOLS = [
"memory-graph",
"save-memory",
"search_memory",
"select-workspace",
"select-space",
"set-active-tag",
"upload-file",
"upload-file-submit",
@ -80,11 +80,22 @@ describeWithAuth("MCP — discovery & identity", () => {
expect(memory?.annotations).toMatchObject(MEMORY_TOOL_ANNOTATIONS)
})
it("lists profile and container-tag resources", async () => {
it("lists profile and space resources", async () => {
const { resources } = await s.client.listResources()
const uris = resources.map((r) => r.uri)
expect(uris).toContain("supermemory://profile")
expect(uris).toContain("supermemory://container-tags")
expect(uris).toContain("supermemory://spaces")
})
it("uses space terminology across exposed MCP metadata", async () => {
const [{ tools }, { resources }, { prompts }] = await Promise.all([
s.client.listTools(),
s.client.listResources(),
s.client.listPrompts(),
])
expect(JSON.stringify({ tools, resources, prompts })).not.toMatch(
/\bworkspaces?\b/i,
)
})
it("lists the context prompt", async () => {
@ -97,6 +108,8 @@ describeWithAuth("MCP — discovery & identity", () => {
expect(res.isError).toBeFalsy()
const parsed = JSON.parse(textOf(res))
expect(parsed.userId).toBeTruthy()
expect(parsed).toHaveProperty("activeSpace")
expect(parsed).not.toHaveProperty("activeWorkspace")
})
it("listSpaces returns content", async () => {

View file

@ -49,25 +49,25 @@ describeWithAuth("MCP — graph, resources & prompts", () => {
const res = await s.client.readResource({ uri: "supermemory://profile" })
expect(res.contents.length).toBeGreaterThan(0)
expect(res.contents[0].mimeType).toBe("text/plain")
expect(res.contents[0].text).toMatch(/# Active Workspace Profile/)
expect(res.contents[0].text).toMatch(/Workspace:/)
expect(res.contents[0].text).toMatch(/# Active Space Profile/)
expect(res.contents[0].text).toMatch(/Space:/)
expect(res.contents[0].text).toMatch(
/Use `listSpaces` to find the relevant workspace key/,
/Use `listSpaces` to find the relevant space key/,
)
})
it("reads all workspaces in a compact human-readable format", async () => {
it("reads all spaces in a compact human-readable format", async () => {
const res = await s.client.readResource({
uri: "supermemory://container-tags",
uri: "supermemory://spaces",
})
const text = res.contents[0].text as string
expect(res.contents[0].mimeType).toBe("text/plain")
expect(text).toMatch(/# My Workspaces/)
expect(text).toMatch(/# My Spaces/)
expect(text).toMatch(/Active:/)
expect(text).not.toMatch(/"containerTags":/)
})
it("gets compact active-workspace context without prompt arguments", async () => {
it("gets compact active-space context without prompt arguments", async () => {
const prompts = await s.client.listPrompts()
const contextPrompt = prompts.prompts.find(
(prompt) => prompt.name === "context",
@ -78,6 +78,6 @@ describeWithAuth("MCP — graph, resources & prompts", () => {
expect(res.messages.length).toBeGreaterThan(0)
const text = res.messages[0].content.text as string
expect(text).toMatch(/# Supermemory Context/)
expect(text).toMatch(/Active workspace:/)
expect(text).toMatch(/Active space:/)
})
})

View file

@ -20,8 +20,8 @@ describe.skipIf(!OAUTH_CREDENTIALS_AVAILABLE)(
await session?.close()
})
it("loads visible workspaces and effective permissions on demand", async () => {
const result = await callTool(session.client, "select-workspace")
it("loads visible spaces and effective permissions on demand", async () => {
const result = await callTool(session.client, "select-space")
expect(result.isError).toBeFalsy()
const content = result.structuredContent as {
view?: string
@ -43,8 +43,8 @@ describe.skipIf(!OAUTH_CREDENTIALS_AVAILABLE)(
).toBe(true)
})
it("shares the selected workspace across MCP transport sessions", async () => {
const picker = await callTool(session.client, "select-workspace")
it("shares the selected space across MCP transport sessions", async () => {
const picker = await callTool(session.client, "select-space")
const pickerContent = picker.structuredContent as {
containerTags?: Array<{ containerTag: string }>
}
@ -65,7 +65,7 @@ describe.skipIf(!OAUTH_CREDENTIALS_AVAILABLE)(
const identity = await callTool(separateSession.client, "whoAmI")
expect(identity.isError).toBeFalsy()
expect(JSON.parse(textOf(identity))).toMatchObject({
activeWorkspace: firstTag,
activeSpace: firstTag,
})
} finally {
await separateSession.close()

View file

@ -11,7 +11,7 @@ const propsOf = (tools: ToolLike[], name: string): Record<string, unknown> =>
const describeWithAuth = describe.skipIf(!OAUTH_CREDENTIALS_AVAILABLE)
describeWithAuth("MCP - workspace scoping", () => {
describeWithAuth("MCP - space scoping", () => {
it("keeps per-call containerTag overrides when an obsolete header is sent", async () => {
const scoped = await connect({
headers: { "x-sm-project": "obsolete-root-scope" },

View file

@ -54,7 +54,7 @@ describe("MCP tool analytics", () => {
)
await harness.invoke(
{ query: "private query", containerTag: "private-workspace" },
{ query: "private query", containerTag: "private-space" },
context,
)
@ -64,14 +64,12 @@ describe("MCP tool analytics", () => {
toolName: "search_memory",
surface: "model_tool",
outcome: "success",
workspaceExplicit: true,
spaceExplicit: true,
client: { name: "claude", version: "1.2.3" },
}),
)
expect(JSON.stringify(record.mock.calls[0])).not.toContain("private query")
expect(JSON.stringify(record.mock.calls[0])).not.toContain(
"private-workspace",
)
expect(JSON.stringify(record.mock.calls[0])).not.toContain("private-space")
expect(JSON.stringify(record.mock.calls[0])).not.toContain("secret result")
})
@ -148,7 +146,7 @@ describe("MCP tool analytics", () => {
surface: "app_launcher",
outcome: "success",
durationMs: 42,
workspaceExplicit: false,
spaceExplicit: false,
},
)
@ -163,7 +161,7 @@ describe("MCP tool analytics", () => {
duration_ms: 42,
mcp_runtime: "stateless",
mcp_surface: "app_launcher",
workspace_explicit: false,
space_explicit: false,
oauth_client_id: "client_123",
},
})

View file

@ -17,7 +17,7 @@ export interface McpToolExecution {
surface: McpToolSurface
outcome: McpToolOutcome
durationMs: number
workspaceExplicit: boolean
spaceExplicit: boolean
client?: { name: string; version?: string }
errorType?: string
}
@ -40,7 +40,7 @@ const TOOL_SURFACES: Record<string, McpToolSurface> = {
listSpaces: "model_tool",
whoAmI: "model_tool",
add_memory: "model_tool",
"select-workspace": "app_launcher",
"select-space": "app_launcher",
"memory-graph": "app_launcher",
"guided-save": "app_launcher",
"upload-file": "app_launcher",
@ -87,7 +87,7 @@ export function posthogEventForToolExecution(
duration_ms: execution.durationMs,
mcp_runtime: "stateless",
mcp_surface: execution.surface,
workspace_explicit: execution.workspaceExplicit,
space_explicit: execution.spaceExplicit,
...(execution.client
? {
mcp_client_name: execution.client.name,
@ -126,7 +126,7 @@ export function createPosthogAnalytics(
}
}
function workspaceWasExplicit(value: unknown): boolean {
function spaceWasExplicit(value: unknown): boolean {
if (!value || typeof value !== "object") return false
const containerTag = Reflect.get(value, "containerTag")
return typeof containerTag === "string" && containerTag.trim().length > 0
@ -185,7 +185,7 @@ export function createTrackedToolServer(
surface: TOOL_SURFACES[name] ?? "model_tool",
outcome,
durationMs: Math.max(0, Math.round(performance.now() - startedAt)),
workspaceExplicit: workspaceWasExplicit(input),
spaceExplicit: spaceWasExplicit(input),
...(client ? { client } : {}),
...(errorType ? { errorType } : {}),
})

View file

@ -6,7 +6,7 @@ describe("SupermemoryClient memory listing", () => {
vi.unstubAllGlobals()
})
it("calls the canonical memory-list endpoint with the selected workspace", async () => {
it("calls the canonical memory-list endpoint with the selected space", async () => {
const responseBody = {
memoryEntries: [
{

View file

@ -4,10 +4,10 @@ export const containerTagSchema = z
.string()
.min(1, "Container tag is required")
.max(128, "Container tag exceeds maximum length")
.describe("Workspace key returned by listSpaces")
.describe("Space key returned by listSpaces")
export const optionalContainerTagSchema = containerTagSchema
.optional()
.describe(
"Workspace key to use for this call. If the user names a workspace, call listSpaces to resolve its key and pass it here. Omit only when the user means the active workspace.",
"Space key to use for this call. If the user names a space, call listSpaces to resolve its key and pass it here. Omit only when the user means the active space.",
)

View file

@ -20,7 +20,7 @@ export function registerContextPrompt(
server.registerPrompt(
"context",
{
description: "Attach compact context for the active workspace",
description: "Attach compact context for the active space",
},
async () => {
try {
@ -37,7 +37,7 @@ export function registerContextPrompt(
const fallback = selectedTag ? "" : " (default)"
const parts: string[] = [
"# Supermemory Context",
`Active workspace: ${activeLabel} [${activeKey}]${fallback}`,
`Active space: ${activeLabel} [${activeKey}]${fallback}`,
]
if (activeWorkspace) {
@ -65,7 +65,7 @@ export function registerContextPrompt(
profileResult.profile.static.length === 0 &&
profileResult.profile.dynamic.length === 0
) {
parts.push("No profile facts are available for this workspace yet.")
parts.push("No profile facts are available for this space yet.")
}
const recentWorkspaces = sortWorkspaces(workspaces, activeKey)
@ -74,7 +74,7 @@ export function registerContextPrompt(
if (recentWorkspaces.length > 0) {
parts.push(
"",
"## Recently Active Workspaces",
"## Recently Active Spaces",
...recentWorkspaces.map((workspace) =>
formatWorkspaceRow(workspace, activeKey, 100),
),
@ -83,7 +83,7 @@ export function registerContextPrompt(
parts.push(
"",
"Use a workspace key with workspace-aware tools when the user asks about another workspace. Keep workspace contexts separate unless the user asks to combine them.",
"Use a space key with space-aware tools when the user asks about another space. Keep space contexts separate unless the user asks to combine them.",
)
return {

View file

@ -11,41 +11,36 @@ export function registerContainerTagsResource(
getClient: () => SupermemoryClient,
resolveContainerTag: () => Promise<string | undefined>,
) {
server.registerResource(
"My Workspaces",
"supermemory://container-tags",
{},
async () => {
const client = getClient()
const [containerTags, selectedTag] = await Promise.all([
client.listContainerTags(),
resolveContainerTag(),
])
const activeKey = selectedTag ?? DEFAULT_PROJECT_ID
const activeWorkspace = containerTags.find(
(workspace) => workspace.containerTag === activeKey,
)
const rows = sortWorkspaces(containerTags, activeKey).map((workspace) =>
formatWorkspaceRow(workspace, activeKey),
)
const activeLabel = workspaceDisplayName(activeWorkspace, activeKey)
const fallback = selectedTag ? "" : " (default)"
const text = [
"# My Workspaces",
`${containerTags.length} available · Active: ${activeLabel} [${activeKey}]${fallback}`,
"",
...rows,
].join("\n")
server.registerResource("My Spaces", "supermemory://spaces", {}, async () => {
const client = getClient()
const [containerTags, selectedTag] = await Promise.all([
client.listContainerTags(),
resolveContainerTag(),
])
const activeKey = selectedTag ?? DEFAULT_PROJECT_ID
const activeWorkspace = containerTags.find(
(workspace) => workspace.containerTag === activeKey,
)
const rows = sortWorkspaces(containerTags, activeKey).map((workspace) =>
formatWorkspaceRow(workspace, activeKey),
)
const activeLabel = workspaceDisplayName(activeWorkspace, activeKey)
const fallback = selectedTag ? "" : " (default)"
const text = [
"# My Spaces",
`${containerTags.length} available · Active: ${activeLabel} [${activeKey}]${fallback}`,
"",
...rows,
].join("\n")
return {
contents: [
{
uri: "supermemory://container-tags",
mimeType: "text/plain",
text,
},
],
}
},
)
return {
contents: [
{
uri: "supermemory://spaces",
mimeType: "text/plain",
text,
},
],
}
})
}

View file

@ -15,7 +15,7 @@ export function registerProfileResource(
resolveContainerTag: () => Promise<string | undefined>,
) {
server.registerResource(
"Active Workspace Profile",
"Active Space Profile",
"supermemory://profile",
{},
async () => {
@ -31,8 +31,8 @@ export function registerProfileResource(
const activeLabel = workspaceDisplayName(activeWorkspace, activeKey)
const fallback = selectedTag ? "" : " (default)"
const parts: string[] = [
"# Active Workspace Profile",
`Workspace: ${activeLabel} [${activeKey}]${fallback}`,
"# Active Space Profile",
`Space: ${activeLabel} [${activeKey}]${fallback}`,
]
if (activeWorkspace) {
@ -60,12 +60,12 @@ export function registerProfileResource(
profileResult.profile.static.length === 0 &&
profileResult.profile.dynamic.length === 0
) {
parts.push("No profile facts are available for this workspace yet.")
parts.push("No profile facts are available for this space yet.")
}
parts.push(
"",
"Other workspaces are available. Use `listSpaces` to find the relevant workspace key, then use that key with workspace-aware tools when the user asks about another workspace. Keep workspace contexts separate unless the user asks to combine them.",
"Other spaces are available. Use `listSpaces` to find the relevant space key, then use that key with space-aware tools when the user asks about another space. Keep space contexts separate unless the user asks to combine them.",
)
return {

View file

@ -17,7 +17,7 @@ export function register(deps: ToolDeps) {
"add_memory",
{
description:
"Add (save) or forget a memory in the user's ACTIVE workspace. Defaults to 'save'. The target workspace is the one the user selected via select-workspace; pass containerTag only to override it. Use 'forget' when information is outdated or the user asks to remove it.",
"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.",
inputSchema,
annotations: MEMORY_TOOL_ANNOTATIONS,
},
@ -38,7 +38,7 @@ export function register(deps: ToolDeps) {
content: [
{
type: "text" as const,
text: `Memory saved (ID: ${result.id}, workspace: ${result.containerTag})`,
text: `Memory saved (ID: ${result.id}, space: ${result.containerTag})`,
},
],
}

View file

@ -17,7 +17,7 @@ export function register(deps: ToolDeps) {
{
title: "Get Document",
description:
"Read one stored document by ID, including its summary and available content. Use listDocuments in the intended workspace to discover document IDs.",
"Read one stored document by ID, including its summary and available content. Use listDocuments in the intended space to discover document IDs.",
inputSchema,
annotations: READ_ONLY_TOOL_ANNOTATIONS,
},

View file

@ -8,7 +8,7 @@ import * as listMemories from "./list-memories"
import * as memoryGraph from "./memory-graph"
import * as saveMemory from "./save-memory"
import * as searchMemory from "./search-memory"
import * as selectWorkspace from "./select-workspace"
import * as selectSpace from "./select-space"
import * as setActiveTag from "./set-active-tag"
import type { ToolDeps } from "./types"
import * as uploadFile from "./upload-file"
@ -22,7 +22,7 @@ export function registerAllTools(deps: ToolDeps) {
listMemories.register(deps)
listContainerTags.register(deps)
whoAmI.register(deps)
selectWorkspace.register(deps)
selectSpace.register(deps)
setActiveTag.register(deps)
memoryGraph.register(deps)
fetchGraphData.register(deps)

View file

@ -7,7 +7,7 @@ export function register(deps: ToolDeps) {
"listSpaces",
{
description:
"List the workspaces available to the user. Returns each workspace's name, key, emoji, document/memory counts, and last activity. Use this first to resolve a named workspace before calling a workspace-aware tool, or when the user asks which workspace may contain something. The list is auto-filtered to workspaces the user can access.",
"List the spaces available to the user. Returns each space's name, key, emoji, document/memory counts, and last activity. Use this first to resolve a named space before calling a space-aware tool, or when the user asks which space may contain something. The list is auto-filtered to spaces the user can access.",
inputSchema: z.object({}),
annotations: READ_ONLY_TOOL_ANNOTATIONS,
},

View file

@ -29,7 +29,7 @@ export function register(deps: ToolDeps) {
{
title: "List Documents",
description:
"List documents in one workspace with their IDs, titles, types, processing status, dates, and summaries. This does not return full document content; use getDocument with an ID from this result to read one document. When the user names a workspace, resolve it with listSpaces and pass containerTag; otherwise use the active workspace.",
"List documents in one space with their IDs, titles, types, processing status, dates, and summaries. This does not return full document content; use getDocument with an ID from this result to read one document. When the user names a space, resolve it with listSpaces and pass containerTag; otherwise use the active space.",
inputSchema,
annotations: READ_ONLY_TOOL_ANNOTATIONS,
},

View file

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

View file

@ -15,7 +15,7 @@ export function register(deps: ToolDeps) {
{
title: "Memory Graph",
description:
"Render the workspace's memory graph directly as an interactive MCP App. This tool is the final visualization; do not create another graph, file, or artifact unless the user explicitly asks for one. When the user names a workspace, resolve it with listSpaces and pass containerTag.",
"Render the space's memory graph directly as an interactive MCP App. This tool is the final visualization; do not create another graph, file, or artifact unless the user explicitly asks for one. When the user names a space, resolve it with listSpaces and pass containerTag.",
inputSchema,
annotations: READ_ONLY_TOOL_ANNOTATIONS,
_meta: appToolMeta(),
@ -44,7 +44,7 @@ export function register(deps: ToolDeps) {
content: [
{
type: "text" as const,
text: `Rendered the interactive Memory Graph MCP App: ${result.documents.length} documents, ${memoryCount} memories${effectiveTag ? `. Workspace: ${effectiveTag}` : ""}. Do not create a duplicate graph or artifact unless the user explicitly requests one.`,
text: `Rendered the interactive Memory Graph MCP App: ${result.documents.length} documents, ${memoryCount} memories${effectiveTag ? `. Space: ${effectiveTag}` : ""}. Do not create a duplicate graph or artifact unless the user explicitly requests one.`,
},
],
structuredContent: sc,

View file

@ -18,7 +18,7 @@ export function register(deps: ToolDeps) {
"search_memory",
{
description:
"Search memories in one workspace with a natural-language query. Returns relevant memories plus that workspace's profile summary. When the user names a workspace, resolve it with listSpaces and pass containerTag; otherwise use the active workspace.",
"Search memories in one space with a natural-language query. Returns relevant memories plus that space's profile summary. When the user names a space, resolve it with listSpaces and pass containerTag; otherwise use the active space.",
inputSchema,
annotations: READ_ONLY_TOOL_ANNOTATIONS,
},

View file

@ -6,11 +6,11 @@ import type { ToolDeps } from "./types"
export function register(deps: ToolDeps) {
deps.server.registerTool(
"select-workspace",
"select-space",
{
title: "Select Workspace",
title: "Select Space",
description:
"Choose which container tag to work in. Shows available container tags as interactive cards.",
"Choose the active Supermemory space. Shows available spaces as interactive cards.",
inputSchema: z.object({}),
_meta: appToolMeta(),
},
@ -40,7 +40,7 @@ export function register(deps: ToolDeps) {
content: [
{
type: "text" as const,
text: `${tags.length} container tags available. Select one to set your active context.`,
text: `${tags.length} spaces available. Select one to set your active context.`,
},
],
structuredContent: sc,

View file

@ -8,7 +8,7 @@ export function register(deps: ToolDeps) {
deps.server.registerTool(
"set-active-tag",
{
description: "Set the active container tag for this account",
description: "Set the active Supermemory space for this account",
inputSchema: z.object({
containerTag: containerTagSchema,
viewId: z.string().uuid().optional(),
@ -35,7 +35,7 @@ export function register(deps: ToolDeps) {
content: [
{
type: "text" as const,
text: `Active workspace set to ${containerTag}`,
text: `Active space set to ${containerTag}`,
},
],
structuredContent: sc,

View file

@ -6,7 +6,7 @@ export function register(deps: ToolDeps) {
deps.server.registerTool(
"whoAmI",
{
description: "Get current user info, role, and workspace context",
description: "Get current user info, role, and space context",
inputSchema: z.object({}),
annotations: READ_ONLY_TOOL_ANNOTATIONS,
},
@ -28,8 +28,8 @@ export function register(deps: ToolDeps) {
name: session.user.name,
role: session.role ?? "unknown",
accessType: session.accessType ?? "full",
activeWorkspace: activeTag ?? null,
assignedTags:
activeSpace: activeTag ?? null,
assignedSpaces:
session.accessType === "restricted"
? session.containerTags
: null,

View file

@ -7,14 +7,14 @@ import {
sortWorkspaces,
} from "./workspace-presentation"
const workspace = (
const space = (
containerTag: string,
lastActivityAt: string | null,
): ContainerTag => ({
id: containerTag,
name: `Workspace ${containerTag}`,
name: `Space ${containerTag}`,
containerTag,
description: "A compact workspace description.",
description: "A compact space description.",
visibility: "private",
createdAt: "2026-01-01T00:00:00.000Z",
updatedAt: "2026-01-01T00:00:00.000Z",
@ -25,13 +25,13 @@ const workspace = (
lastActivityAt,
})
describe("workspace presentation", () => {
it("keeps the active workspace first, then sorts by activity", () => {
describe("space presentation", () => {
it("keeps the active space first, then sorts by activity", () => {
const sorted = sortWorkspaces(
[
workspace("older", "2026-01-01T00:00:00.000Z"),
workspace("active", "2025-01-01T00:00:00.000Z"),
workspace("newer", "2026-02-01T00:00:00.000Z"),
space("older", "2026-01-01T00:00:00.000Z"),
space("active", "2025-01-01T00:00:00.000Z"),
space("newer", "2026-02-01T00:00:00.000Z"),
],
"active",
)
@ -45,7 +45,7 @@ describe("workspace presentation", () => {
it("formats compact rows without internal database IDs", () => {
const row = formatWorkspaceRow(
workspace("project-key", "2026-07-29T19:44:28.177Z"),
space("project-key", "2026-07-29T19:44:28.177Z"),
"project-key",
)

View file

@ -35,11 +35,11 @@ describe("workspace application state", () => {
).resolves.toBeUndefined()
})
it("tells the model how to route explicit workspace requests", () => {
it("tells the model how to route explicit space requests", () => {
expect(optionalContainerTagSchema.description).toContain(
"If the user names a workspace",
"If the user names a space",
)
expect(optionalContainerTagSchema.description).toContain("listSpaces")
expect(optionalContainerTagSchema.description).toContain("active workspace")
expect(optionalContainerTagSchema.description).toContain("active space")
})
})

View file

@ -24,14 +24,14 @@ interface WorkspaceSelectProps {
}
// Ports console-v2's ChipSelect/ContainerTagSelect: a Popover-anchored,
// searchable list. Scales to hundreds of workspaces where chips can't.
// searchable list. Scales to hundreds of spaces where chips can't.
export function WorkspaceSelect({
value,
onValueChange,
options,
placeholder = "Select workspace",
emptyText = "No workspaces",
searchPlaceholder = "Search workspaces…",
placeholder = "Select space",
emptyText = "No spaces",
searchPlaceholder = "Search spaces…",
disabled = false,
className,
}: WorkspaceSelectProps) {

View file

@ -3,7 +3,7 @@
*
* "sm_project_marketing" "Marketing"
* "sm_project_eng_rfcs" "Eng Rfcs"
* "my_custom_workspace" "My Custom Workspace"
* "my_custom_space" "My Custom Space"
*/
export function formatTagLabel(raw: string): string {
const slug = raw.replace(/^sm_project_/i, "").replace(/^sm_/i, "")

View file

@ -76,7 +76,7 @@ describe("view checkpoints", () => {
saveViewCheckpoint(view)
expect(setWidgetState).toHaveBeenCalledWith({
modelContent: 'Supermemory active workspace is now "model_test".',
modelContent: 'Supermemory active space is now "model_test".',
privateContent: {
existing: true,
supermemoryView: view,

View file

@ -48,11 +48,11 @@ function getStorage(): Storage | undefined {
function modelContentForView(view: ViewMessage): string {
switch (view.view) {
case "confirmation":
return `Supermemory active workspace is now "${view.containerTag}".`
return `Supermemory active space is now "${view.containerTag}".`
case "save-success":
return `A memory was saved to Supermemory workspace "${view.containerTag}" with memory ID "${view.id}".`
return `A memory was saved to Supermemory space "${view.containerTag}" with memory ID "${view.id}".`
case "upload-success":
return `"${view.fileName}" was uploaded to Supermemory workspace "${view.containerTag}" with document ID "${view.id}".`
return `"${view.fileName}" was uploaded to Supermemory space "${view.containerTag}" with document ID "${view.id}".`
default:
return "Supermemory widget state updated."
}

View file

@ -331,8 +331,8 @@ export function Studio() {
</div>
<Card className="max-w-sm">
<Field
hint="Searchable Popover — scales to hundreds of workspaces."
label="Workspace select"
hint="Searchable Popover — scales to hundreds of spaces."
label="Space select"
>
<WorkspaceSelect
onValueChange={setSelectValue}

View file

@ -18,12 +18,12 @@ export function Confirmation({ containerTag }: Props) {
</span>
<Stack align="center" gap="xs">
<div className="text-(length:--text-sm) font-semibold text-text-primary">
Active workspace set
Active space set
</div>
<WorkspaceChip containerTag={containerTag} />
</Stack>
<p className="max-w-xs text-(length:--text-xs) leading-relaxed text-text-muted">
Saves and recalls will use this workspace until you change it.
Saves and recalls will use this space until you change it.
</p>
</Stack>
)

View file

@ -56,17 +56,17 @@ export function Picker({
setPending(null)
if (!result.ok || !result.data) {
log("error", `[picker] set-active-tag failed: ${result.error}`)
onError(result.error ?? "Failed to set active workspace")
onError(result.error ?? "Failed to set active space")
return
}
onAdvance(result.data)
const handoff = await handoffToModel({
context: `Supermemory workspace selection changed. Active workspace: "${containerTag}". Use it for future Supermemory actions until another workspace is selected.`,
message: `I selected "${containerTag}" as my active Supermemory workspace. Use this workspace for future Supermemory actions until I select another one.`,
context: `Supermemory space selection changed. Active space: "${containerTag}". Use it for future Supermemory actions until another space is selected.`,
message: `I selected "${containerTag}" as my active Supermemory space. Use this space for future Supermemory actions until I select another one.`,
structuredContent: {
supermemory: {
action: "workspace-selected",
activeWorkspace: containerTag,
action: "space-selected",
activeSpace: containerTag,
},
},
})
@ -87,22 +87,22 @@ export function Picker({
const count = containerTags.length
const description =
count === 0
? "Create a workspace in Supermemory to get started."
: "Pick the workspace to save and recall from."
? "Create a space in Supermemory to get started."
: "Pick the space to save and recall from."
return (
<div className="flex flex-col">
<PageHeader description={description} title="Workspaces" />
<PageHeader description={description} title="Spaces" />
<div className="flex flex-col gap-(--space-3) px-(--page-header-px) pb-(--space-6)">
{count === 0 ? (
<div className="flex flex-col items-center gap-(--space-2) rounded-xl border border-border bg-[var(--card-bg)] px-(--space-6) py-(--space-10) text-center">
<Package className="size-7 text-text-muted" />
<p className="text-(length:--text-sm) font-medium text-text-primary">
No workspaces yet
No spaces yet
</p>
<p className="max-w-xs text-(length:--text-xs) leading-relaxed text-text-muted">
Workspaces you create in Supermemory show up here, ready to save
and recall from.
Spaces you create in Supermemory show up here, ready to save and
recall from.
</p>
</div>
) : (
@ -113,7 +113,7 @@ export function Picker({
<Input
className="pl-(--space-8)"
onChange={(e) => setQuery(e.target.value)}
placeholder="Search workspaces…"
placeholder="Search spaces…"
value={query}
/>
</div>
@ -122,7 +122,7 @@ export function Picker({
{filtered.length === 0 ? (
<div className="workspace-picker-grid items-center justify-center py-(--space-8)">
<p className="px-(--space-4) text-center text-(length:--text-sm) text-text-muted">
No workspaces match {query}.
No spaces match {query}.
</p>
</div>
) : (
@ -148,7 +148,7 @@ export function Picker({
{pending ? (
<p className="text-(length:--text-xs) text-text-muted">
Setting workspace to {formatTagLabel(pending)}
Setting space to {formatTagLabel(pending)}
</p>
) : null}
</div>

View file

@ -76,12 +76,12 @@ export function Save({
result.data.view === "save-success" ? result.data.id : undefined
onAdvance(result.data)
const handoff = await handoffToModel({
context: `Supermemory widget action completed. A memory was saved to workspace "${selectedTag}"${memoryId ? ` with memory ID "${memoryId}"` : ""}. Saved content:\n\n${trimmed}\n\nIt is already saved; do not save it again.`,
message: `I used the Supermemory widget to save a memory to workspace "${selectedTag}"${memoryId ? ` (memory ID: ${memoryId})` : ""}. The memory is already saved; do not save it again.`,
context: `Supermemory widget action completed. A memory was saved to space "${selectedTag}"${memoryId ? ` with memory ID "${memoryId}"` : ""}. Saved content:\n\n${trimmed}\n\nIt is already saved; do not save it again.`,
message: `I used the Supermemory widget to save a memory to space "${selectedTag}"${memoryId ? ` (memory ID: ${memoryId})` : ""}. The memory is already saved; do not save it again.`,
structuredContent: {
supermemory: {
action: "memory-saved",
activeWorkspace: selectedTag,
activeSpace: selectedTag,
memoryId,
content: trimmed,
},
@ -104,7 +104,7 @@ export function Save({
return (
<div className="flex flex-col">
<PageHeader
description="Capture a thought to a workspace your team can search later."
description="Capture a thought to a space your team can search later."
title="Add Memory"
/>
<div className="px-(--page-header-px) pb-(--space-6)">
@ -120,7 +120,7 @@ export function Save({
</Field>
{writableTags.length > 0 ? (
<Field label="Workspace">
<Field label="Space">
<WorkspaceSelect
onValueChange={setSelectedTag}
options={options}

View file

@ -80,12 +80,12 @@ export function Upload({
result.data.view === "upload-success" ? result.data.id : undefined
onAdvance(result.data)
const handoff = await handoffToModel({
context: `Supermemory widget action completed. "${file.name}" was uploaded to workspace "${selectedTag}"${documentId ? ` with document ID "${documentId}"` : ""}. It is already uploaded; do not upload it again.`,
message: `I used the Supermemory widget to upload "${file.name}" to workspace "${selectedTag}"${documentId ? ` (document ID: ${documentId})` : ""}. The file is already uploaded; do not upload it again.`,
context: `Supermemory widget action completed. "${file.name}" was uploaded to space "${selectedTag}"${documentId ? ` with document ID "${documentId}"` : ""}. It is already uploaded; do not upload it again.`,
message: `I used the Supermemory widget to upload "${file.name}" to space "${selectedTag}"${documentId ? ` (document ID: ${documentId})` : ""}. The file is already uploaded; do not upload it again.`,
structuredContent: {
supermemory: {
action: "file-uploaded",
activeWorkspace: selectedTag,
activeSpace: selectedTag,
documentId,
fileName: file.name,
},
@ -114,7 +114,7 @@ export function Upload({
return (
<div className="flex flex-col">
<PageHeader
description="Send a file (text, PDF, image, video) into a workspace."
description="Send a file (text, PDF, image, video) into a space."
title="Upload File"
/>
<div className="px-(--page-header-px) pb-(--space-6)">
@ -150,7 +150,7 @@ export function Upload({
)}
{writableTags.length > 0 ? (
<Field label="Workspace">
<Field label="Space">
<WorkspaceSelect
onValueChange={setSelectedTag}
options={options}