From a99cf4f7e186346415bebee8ea07174626f1dd55 Mon Sep 17 00:00:00 2001
From: Prasanna721 <106952318+Prasanna721@users.noreply.github.com>
Date: Tue, 4 Aug 2026 18:58:14 +0000
Subject: [PATCH 02/70] fix mcp graph and file uploads (#1397)
Fixes cross-host graph rendering and moves widget uploads off the JSON/base64 tool transport.
- render graph data from the launcher result without a second tool call
- stream multipart uploads through one-time, short-lived upload sessions
- remove temporary widget diagnostics and redundant unit tests
Tested with Biome, TypeScript, 17 unit tests, a Wrangler deployment dry-run, and live graph rendering in ChatGPT and Claude. Authenticated E2E setup is currently blocked by the saved OAuth refresh session returning `invalid_grant: session not found`.
---
apps/mcp/README.md | 2 +-
apps/mcp/e2e/discovery.test.ts | 2 +-
apps/mcp/e2e/graph.test.ts | 4 +-
apps/mcp/e2e/list-memories.test.ts | 45 ++--
apps/mcp/src/format.test.ts | 219 ------------------
apps/mcp/src/server/analytics.ts | 2 +-
apps/mcp/src/server/app-metadata.test.ts | 33 ---
apps/mcp/src/server/client/index.test.ts | 63 -----
apps/mcp/src/server/client/index.ts | 42 ----
apps/mcp/src/server/index.ts | 62 ++++-
apps/mcp/src/server/resources/widget.ts | 55 +++--
apps/mcp/src/server/server.ts | 23 +-
.../mcp/src/server/space-presentation.test.ts | 64 -----
apps/mcp/src/server/space-state.ts | 58 +++++
apps/mcp/src/server/tools/index.ts | 4 +-
apps/mcp/src/server/tools/memory-graph.ts | 12 +-
.../src/server/tools/prepare-file-upload.ts | 30 +++
apps/mcp/src/server/tools/types.ts | 7 +
.../src/server/tools/upload-file-submit.ts | 61 -----
apps/mcp/src/server/types.ts | 1 +
.../src/server/widget-resource-metadata.ts | 40 ++--
apps/mcp/src/shared/types.ts | 23 +-
apps/mcp/src/widget/App.tsx | 20 +-
apps/mcp/src/widget/ErrorBoundary.tsx | 29 +--
apps/mcp/src/widget/McpAppProvider.tsx | 23 --
apps/mcp/src/widget/hooks/useApp.ts | 7 -
apps/mcp/src/widget/hooks/useLog.ts | 18 --
apps/mcp/src/widget/lib/modelHandoff.test.ts | 107 ---------
apps/mcp/src/widget/lib/readFileAsBase64.ts | 19 --
.../mcp/src/widget/lib/viewCheckpoint.test.ts | 105 ---------
apps/mcp/src/widget/studio/Studio.tsx | 4 +-
apps/mcp/src/widget/views/Graph.tsx | 70 +-----
apps/mcp/src/widget/views/Picker.tsx | 18 +-
apps/mcp/src/widget/views/Save.tsx | 18 +-
apps/mcp/src/widget/views/Upload.tsx | 91 ++++----
35 files changed, 351 insertions(+), 1030 deletions(-)
delete mode 100644 apps/mcp/src/format.test.ts
delete mode 100644 apps/mcp/src/server/app-metadata.test.ts
delete mode 100644 apps/mcp/src/server/client/index.test.ts
delete mode 100644 apps/mcp/src/server/space-presentation.test.ts
create mode 100644 apps/mcp/src/server/tools/prepare-file-upload.ts
delete mode 100644 apps/mcp/src/server/tools/upload-file-submit.ts
delete mode 100644 apps/mcp/src/widget/hooks/useLog.ts
delete mode 100644 apps/mcp/src/widget/lib/modelHandoff.test.ts
delete mode 100644 apps/mcp/src/widget/lib/readFileAsBase64.ts
delete mode 100644 apps/mcp/src/widget/lib/viewCheckpoint.test.ts
diff --git a/apps/mcp/README.md b/apps/mcp/README.md
index bcceafbe..1cf39bc1 100644
--- a/apps/mcp/README.md
+++ b/apps/mcp/README.md
@@ -73,7 +73,7 @@ These tools are available to the embedded MCP App and hidden from the model.
| --- | --- |
| `set-active-tag` | Persist the selected active space |
| `save-memory` | Submit the guided save form |
-| `upload-file-submit` | Submit an encoded file upload |
+| `prepare-file-upload` | Prepare a secure direct file upload |
| `fetch-graph-data` | Fetch graph documents for the app |
## Resources And Prompt
diff --git a/apps/mcp/e2e/discovery.test.ts b/apps/mcp/e2e/discovery.test.ts
index b9ac670f..3da201c2 100644
--- a/apps/mcp/e2e/discovery.test.ts
+++ b/apps/mcp/e2e/discovery.test.ts
@@ -16,12 +16,12 @@ const EXPECTED_TOOLS = [
"listMemories",
"listSpaces",
"memory-graph",
+ "prepare-file-upload",
"save-memory",
"search_memory",
"select-space",
"set-active-tag",
"upload-file",
- "upload-file-submit",
"whoAmI",
]
const describeWithAuth = describe.skipIf(!OAUTH_CREDENTIALS_AVAILABLE)
diff --git a/apps/mcp/e2e/graph.test.ts b/apps/mcp/e2e/graph.test.ts
index 74d76d92..5b9d45a5 100644
--- a/apps/mcp/e2e/graph.test.ts
+++ b/apps/mcp/e2e/graph.test.ts
@@ -1,5 +1,5 @@
import { afterAll, beforeAll, describe, expect, it } from "vitest"
-import { graphResultMetaSchema, graphViewSchema } from "../src/shared/types"
+import { graphViewSchema } from "../src/shared/types"
import {
OAUTH_CREDENTIALS_AVAILABLE,
callTool,
@@ -29,8 +29,6 @@ describeWithAuth("MCP — graph, resources & prompts", () => {
expect(result.data.view).toBe("graph")
expect(result.data.rendered).toBe(true)
expect(result.data.documentCount).toBe(result.data.documents.length)
- const resultMeta = graphResultMetaSchema.safeParse(res._meta)
- expect(resultMeta.success).toBe(true)
})
it("fetch-graph-data returns paginated documents", async () => {
diff --git a/apps/mcp/e2e/list-memories.test.ts b/apps/mcp/e2e/list-memories.test.ts
index a4adad83..e171cc9b 100644
--- a/apps/mcp/e2e/list-memories.test.ts
+++ b/apps/mcp/e2e/list-memories.test.ts
@@ -1,5 +1,9 @@
import { randomUUID } from "node:crypto"
import { afterAll, beforeAll, describe, expect, it } from "vitest"
+import {
+ uploadPreparationSchema,
+ uploadResponseSchema,
+} from "../src/shared/types"
import {
OAUTH_CREDENTIALS_AVAILABLE,
callTool,
@@ -153,29 +157,34 @@ describe.skipIf(!OAUTH_CREDENTIALS_AVAILABLE)(
throw new Error("Upload did not provide a writable space")
}
- const uploaded = await callTool(session.client, "upload-file-submit", {
- fileData: Buffer.from(fileContent).toString("base64"),
- fileName,
- mimeType: "text/plain",
- containerTag,
- viewId: launcherView.viewId,
+ const prepared = await callTool(session.client, "prepare-file-upload")
+ expect(prepared.isError).toBeFalsy()
+ const preparation = uploadPreparationSchema.parse(
+ prepared.structuredContent,
+ )
+ const formData = new FormData()
+ formData.append("file", new Blob([fileContent]), fileName)
+ formData.append("containerTag", containerTag)
+ formData.append(
+ "metadata",
+ JSON.stringify({ sm_source: "supermemory-mcp" }),
+ )
+
+ const response = await fetch(preparation.uploadUrl, {
+ method: "POST",
+ headers: {
+ Authorization: `Bearer ${preparation.uploadToken}`,
+ },
+ body: formData,
})
- expect(uploaded.isError).toBeFalsy()
- const uploadedView = uploaded.structuredContent as AppView
- expect(uploadedView).toMatchObject({
- view: "upload-success",
- fileName,
- containerTag,
- })
- expect(uploadedView.id).toBeTruthy()
- if (!uploadedView.id)
- throw new Error("Upload did not return a document ID")
+ expect(response.ok).toBe(true)
+ const uploaded = uploadResponseSchema.parse(await response.json())
const document = await waitForToolText(
session,
"getDocument",
- { documentId: uploadedView.id },
- `Document ID: ${uploadedView.id}`,
+ { documentId: uploaded.id },
+ `Document ID: ${uploaded.id}`,
20,
1000,
)
diff --git a/apps/mcp/src/format.test.ts b/apps/mcp/src/format.test.ts
deleted file mode 100644
index 097946b2..00000000
--- a/apps/mcp/src/format.test.ts
+++ /dev/null
@@ -1,219 +0,0 @@
-import { describe, expect, it } from "vitest"
-import type {
- DocumentDetails,
- DocumentsListResponse,
- MemoryEntriesResponse,
-} from "./server/client"
-import {
- formatDocument,
- formatDocumentsList,
- formatMemoryEntriesList,
-} from "./server/format"
-
-function makeDocumentsResponse(
- overrides: Partial
= {},
-): DocumentsListResponse {
- return {
- documents: [],
- pagination: { currentPage: 1, limit: 10, totalItems: 0, totalPages: 1 },
- ...overrides,
- }
-}
-
-function makeMemoryResponse(
- overrides: Partial = {},
-): MemoryEntriesResponse {
- return {
- memoryEntries: [],
- pagination: { currentPage: 1, limit: 10, totalItems: 0, totalPages: 1 },
- ...overrides,
- }
-}
-
-function makeMemory(memory: string, extra: Record = {}) {
- return {
- id: `mem_${memory.slice(0, 8)}`,
- memory,
- version: 1,
- isLatest: true,
- isForgotten: false,
- createdAt: "2026-06-10T12:00:00Z",
- updatedAt: "2026-06-10T12:00:00Z",
- ...extra,
- }
-}
-
-describe("formatDocumentsList", () => {
- it("reports an empty document store", () => {
- expect(formatDocumentsList(makeDocumentsResponse())).toBe(
- "No documents stored yet.",
- )
- })
-
- it("formats document metadata and stable IDs without content", () => {
- const result = formatDocumentsList(
- makeDocumentsResponse({
- documents: [
- {
- id: "doc_1",
- connectionId: null,
- createdAt: "2026-06-12T08:00:00Z",
- customId: null,
- metadata: null,
- status: "done",
- summary: "A compact summary.",
- title: "Preferences",
- type: "text",
- updatedAt: "2026-06-12T08:00:00Z",
- },
- ],
- pagination: { currentPage: 1, limit: 10, totalItems: 1, totalPages: 1 },
- }),
- )
-
- expect(result).toContain(
- "1 document (page 1 of 1, 1 document total), newest first.",
- )
- expect(result).toContain('- [doc_1] "Preferences" (text, done, 2026-06-12)')
- expect(result).toContain("Summary: A compact summary.")
- expect(result).toContain(
- "Use getDocument with a document ID to read its content.",
- )
- })
-
- it("points to the next document page", () => {
- const result = formatDocumentsList(
- makeDocumentsResponse({
- documents: [
- {
- id: "doc_1",
- connectionId: null,
- createdAt: "2026-06-12T08:00:00Z",
- customId: null,
- metadata: null,
- status: "done",
- summary: null,
- title: null,
- type: "text",
- updatedAt: "2026-06-12T08:00:00Z",
- },
- ],
- pagination: { currentPage: 1, limit: 1, totalItems: 3, totalPages: 3 },
- }),
- )
-
- expect(result).toContain('"(untitled)"')
- expect(result).toContain(
- "More available - call listDocuments with page: 2.",
- )
- })
-})
-
-describe("formatMemoryEntriesList", () => {
- it("reports an empty memory store", () => {
- expect(formatMemoryEntriesList(makeMemoryResponse())).toBe(
- "No active memories stored yet.",
- )
- })
-
- it("formats active memories independently of documents", () => {
- const result = formatMemoryEntriesList(
- makeMemoryResponse({
- memoryEntries: [
- makeMemory("User prefers dark mode", {
- id: "mem_1",
- version: 2,
- documentIds: ["doc_1", "doc_2"],
- history: [
- {
- id: "mem_old",
- memory: "User sometimes uses dark mode",
- version: 1,
- createdAt: "2026-06-01T00:00:00Z",
- updatedAt: "2026-06-01T00:00:00Z",
- },
- ],
- }),
- ],
- pagination: { currentPage: 1, limit: 10, totalItems: 1, totalPages: 1 },
- }),
- )
-
- expect(result).toContain(
- "1 active memory (page 1 of 1, 1 memory entry total), newest first.",
- )
- expect(result).toContain("- [mem_1] User prefers dark mode")
- expect(result).toContain(
- "version 2 | updated 2026-06-10 | 1 previous version",
- )
- expect(result).toContain("Source documents: doc_1, doc_2")
- })
-
- it("excludes forgotten and superseded entries", () => {
- const result = formatMemoryEntriesList(
- makeMemoryResponse({
- memoryEntries: [
- makeMemory("Current fact"),
- makeMemory("Forgotten fact", { isForgotten: true }),
- makeMemory("Old fact", { isLatest: false }),
- ],
- pagination: { currentPage: 1, limit: 10, totalItems: 3, totalPages: 1 },
- }),
- )
-
- expect(result).toContain("Current fact")
- expect(result).not.toContain("Forgotten fact")
- expect(result).not.toContain("Old fact")
- })
-
- it("flattens and truncates oversized memory text", () => {
- const result = formatMemoryEntriesList(
- makeMemoryResponse({
- memoryEntries: [
- makeMemory("line one\nline two"),
- makeMemory(`start ${"x".repeat(600)}`),
- ],
- pagination: { currentPage: 1, limit: 10, totalItems: 2, totalPages: 1 },
- }),
- )
-
- expect(result).toContain("line one line two")
- expect(result).toContain("... [truncated]")
- })
-})
-
-describe("formatDocument", () => {
- const document: DocumentDetails = {
- id: "doc_1",
- connectionId: null,
- content: "Original input",
- createdAt: "2026-06-12T08:00:00Z",
- customId: null,
- metadata: null,
- ogImage: null,
- raw: "Full extracted document text",
- source: "text",
- spatialPoint: null,
- status: "done",
- summary: "A compact summary.",
- title: "Preferences",
- type: "text",
- updatedAt: "2026-06-12T09:00:00Z",
- url: null,
- }
-
- it("returns document metadata, summary, and full available content", () => {
- const result = formatDocument(document)
-
- expect(result).toContain("# Preferences")
- expect(result).toContain("Document ID: doc_1")
- expect(result).toContain("## Summary\nA compact summary.")
- expect(result).toContain("## Content\nFull extracted document text")
- expect(result).not.toContain("Original input")
- })
-
- it("falls back to the original content when raw content is absent", () => {
- const result = formatDocument({ ...document, raw: null })
- expect(result).toContain("## Content\nOriginal input")
- })
-})
diff --git a/apps/mcp/src/server/analytics.ts b/apps/mcp/src/server/analytics.ts
index 6e7fd548..aac932d1 100644
--- a/apps/mcp/src/server/analytics.ts
+++ b/apps/mcp/src/server/analytics.ts
@@ -46,7 +46,7 @@ const TOOL_SURFACES: Record = {
"upload-file": "app_launcher",
"set-active-tag": "app_action",
"save-memory": "app_action",
- "upload-file-submit": "app_action",
+ "prepare-file-upload": "app_action",
"fetch-graph-data": "app_internal",
}
diff --git a/apps/mcp/src/server/app-metadata.test.ts b/apps/mcp/src/server/app-metadata.test.ts
deleted file mode 100644
index 74ab25dc..00000000
--- a/apps/mcp/src/server/app-metadata.test.ts
+++ /dev/null
@@ -1,33 +0,0 @@
-import { describe, expect, it } from "vitest"
-import {
- appResultMeta,
- appToolMeta,
- SUPERMEMORY_RESOURCE_URI,
-} from "./app-metadata"
-
-describe("MCP Apps metadata compatibility", () => {
- it("advertises both current and legacy resource URI metadata", () => {
- expect(SUPERMEMORY_RESOURCE_URI).toMatch(
- /^ui:\/\/supermemory\/app-[a-f0-9]{64}\.html$/,
- )
- expect(appToolMeta()).toEqual({
- ui: { resourceUri: SUPERMEMORY_RESOURCE_URI },
- "ui/resourceUri": SUPERMEMORY_RESOURCE_URI,
- })
- })
-
- it("keeps App-only tools hidden from the model", () => {
- expect(appToolMeta(["app"])).toMatchObject({
- ui: {
- resourceUri: SUPERMEMORY_RESOURCE_URI,
- visibility: ["app"],
- },
- })
- })
-
- it("provides a stable ChatGPT widget-state key", () => {
- expect(appResultMeta("view-123")).toEqual({
- "openai/widgetSessionId": "view-123",
- })
- })
-})
diff --git a/apps/mcp/src/server/client/index.test.ts b/apps/mcp/src/server/client/index.test.ts
deleted file mode 100644
index 19c2de2d..00000000
--- a/apps/mcp/src/server/client/index.test.ts
+++ /dev/null
@@ -1,63 +0,0 @@
-import { afterEach, describe, expect, it, vi } from "vitest"
-import { SupermemoryClient } from "."
-
-describe("SupermemoryClient memory listing", () => {
- afterEach(() => {
- vi.unstubAllGlobals()
- })
-
- it("calls the canonical memory-list endpoint with the selected space", async () => {
- const responseBody = {
- memoryEntries: [
- {
- id: "mem_1",
- memory: "User prefers dark mode",
- version: 1,
- isLatest: true,
- isForgotten: false,
- createdAt: "2026-07-29T00:00:00.000Z",
- updatedAt: "2026-07-29T00:00:00.000Z",
- history: [],
- documentIds: ["doc_1"],
- },
- ],
- pagination: {
- currentPage: 2,
- limit: 20,
- totalItems: 21,
- totalPages: 2,
- },
- }
- const fetchMock = vi.fn().mockResolvedValue(
- new Response(JSON.stringify(responseBody), {
- status: 200,
- headers: { "Content-Type": "application/json" },
- }),
- )
- vi.stubGlobal("fetch", fetchMock)
-
- const client = new SupermemoryClient(
- "oauth-token",
- "snowcone_grande",
- "https://api.example.com",
- )
- await expect(client.listMemoryEntries(2, 20)).resolves.toEqual(responseBody)
-
- expect(fetchMock).toHaveBeenCalledOnce()
- const [url, init] = fetchMock.mock.calls[0] as [string, RequestInit]
- expect(url).toBe("https://api.example.com/v4/memories/list")
- expect(init.method).toBe("POST")
- expect(init.headers).toMatchObject({
- Authorization: "Bearer oauth-token",
- "Content-Type": "application/json",
- "x-sm-source": "supermemory-mcp",
- })
- expect(JSON.parse(init.body as string)).toEqual({
- containerTags: ["snowcone_grande"],
- page: 2,
- limit: 20,
- sort: "createdAt",
- order: "desc",
- })
- })
-})
diff --git a/apps/mcp/src/server/client/index.ts b/apps/mcp/src/server/client/index.ts
index d3c7bc54..cc45d438 100644
--- a/apps/mcp/src/server/client/index.ts
+++ b/apps/mcp/src/server/client/index.ts
@@ -122,11 +122,6 @@ const sdkResultSchema = z.looseObject({
context: z.string().nullish(),
})
-const uploadResultSchema = z.object({
- id: z.string(),
- status: z.string(),
-})
-
function mapSdkResults(value: unknown): Memory[] {
return z
.array(sdkResultSchema)
@@ -450,43 +445,6 @@ export class SupermemoryClient {
}
}
- async uploadFile(
- fileData: ArrayBuffer,
- fileName: string,
- mimeType: string,
- containerTag?: string,
- ): Promise<{ id: string; status: string }> {
- try {
- const formData = new FormData()
- const blob = new Blob([fileData], { type: mimeType })
- formData.append("file", blob, fileName)
- if (containerTag) {
- formData.append("containerTags", containerTag)
- }
- formData.append("metadata", JSON.stringify({ sm_source: MCP_SOURCE }))
-
- const response = await fetch(`${this.apiUrl}/v3/documents/file`, {
- method: "POST",
- headers: {
- Authorization: `Bearer ${this.bearerToken}`,
- "x-sm-source": MCP_SOURCE,
- },
- body: formData,
- })
-
- if (!response.ok) {
- const text = await response.text()
- throw Object.assign(new Error(text || "Upload failed"), {
- status: response.status,
- })
- }
-
- return uploadResultSchema.parse(await response.json())
- } catch (error) {
- this.handleError(error)
- }
- }
-
private handleError(error: unknown): never {
// Handle request timeout (AbortSignal.timeout or explicit abort)
if (
diff --git a/apps/mcp/src/server/index.ts b/apps/mcp/src/server/index.ts
index dafdc9bc..f435ea3a 100644
--- a/apps/mcp/src/server/index.ts
+++ b/apps/mcp/src/server/index.ts
@@ -6,7 +6,7 @@ import { validateOAuthToken, type AuthUser } from "./auth"
import { SupermemoryMCP } from "./legacy-protocol-state"
import { createSupermemoryServer } from "./server"
import type { ActorContext, ServerEnv } from "./types"
-import { SpaceState } from "./space-state"
+import { SpaceState, uploadStateName } from "./space-state"
type Bindings = ServerEnv
@@ -16,6 +16,8 @@ const DEFAULT_API_URL = "https://api.supermemory.ai"
const DEFAULT_MCP_RESOURCE = "https://mcp.supermemory.ai/mcp"
const PROTECTED_RESOURCE_METADATA_PATH =
"/.well-known/oauth-protected-resource/mcp"
+const UPLOAD_ID_PATTERN =
+ /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i
const DEFAULT_ALLOWED_ORIGIN_HOSTNAMES = [
"app.supermemory.ai",
"mcp.supermemory.ai",
@@ -170,6 +172,7 @@ async function handleMcpRequest(
const resourceMetadataUrl = reqHost
? `${reqProto}://${reqHost}${PROTECTED_RESOURCE_METADATA_PATH}`
: PROTECTED_RESOURCE_METADATA_PATH
+ const mcpOrigin = c.env.MCP_PUBLIC_ORIGIN || new URL(mcpResource).origin
if (!token) return unauthorizedResponse(resourceMetadataUrl)
@@ -187,8 +190,11 @@ async function handleMcpRequest(
: c.req.raw
const handler = createMcpHandler(
() =>
- createSupermemoryServer(c.env, actor, (promise) =>
- c.executionCtx.waitUntil(promise),
+ createSupermemoryServer(
+ c.env,
+ actor,
+ (promise) => c.executionCtx.waitUntil(promise),
+ mcpOrigin,
),
{
route: "/mcp",
@@ -204,6 +210,56 @@ async function handleMcpRequest(
})
}
+app.post("/upload/:uploadId", async (c) => {
+ const uploadId = c.req.param("uploadId")
+ const contentType = c.req.header("Content-Type")
+ const authHeader = c.req.header("Authorization")
+ const uploadToken = authHeader?.replace(/^Bearer\s+/i, "").trim()
+
+ if (!UPLOAD_ID_PATTERN.test(uploadId) || !uploadToken) {
+ return c.json({ error: "Invalid or expired upload session" }, 401)
+ }
+ if (!contentType?.toLowerCase().startsWith("multipart/form-data;")) {
+ return c.json({ error: "Expected multipart form data" }, 415)
+ }
+ if (!c.req.raw.body) {
+ return c.json({ error: "File upload body is required" }, 400)
+ }
+
+ const uploadState = c.env.SPACE_STATE.getByName(uploadStateName(uploadId))
+ const session = await uploadState.consumeUploadSession(uploadToken)
+ if (!session) {
+ return c.json({ error: "Invalid or expired upload session" }, 401)
+ }
+
+ const apiUrl = (c.env.API_URL || DEFAULT_API_URL).replace(/\/+$/, "")
+ try {
+ const response = await fetch(`${apiUrl}/v3/documents/file`, {
+ method: "POST",
+ headers: {
+ Authorization: `Bearer ${session.bearerToken}`,
+ "Content-Type": contentType,
+ "x-sm-source": "supermemory-mcp",
+ },
+ body: c.req.raw.body,
+ signal: c.req.raw.signal,
+ })
+ const headers = new Headers({ "Cache-Control": "no-store" })
+ const responseContentType = response.headers.get("Content-Type")
+ const retryAfter = response.headers.get("Retry-After")
+ if (responseContentType) headers.set("Content-Type", responseContentType)
+ if (retryAfter) headers.set("Retry-After", retryAfter)
+
+ return new Response(response.body, {
+ status: response.status,
+ statusText: response.statusText,
+ headers,
+ })
+ } catch {
+ return c.json({ error: "File upload failed" }, 502)
+ }
+})
+
app.all("/", (c) => handleMcpRequest(c, "/mcp"))
app.all("/mcp", (c) => handleMcpRequest(c))
app.all("/mcp/", (c) => handleMcpRequest(c, "/mcp"))
diff --git a/apps/mcp/src/server/resources/widget.ts b/apps/mcp/src/server/resources/widget.ts
index ed817318..e8fb3eb4 100644
--- a/apps/mcp/src/server/resources/widget.ts
+++ b/apps/mcp/src/server/resources/widget.ts
@@ -1,4 +1,4 @@
-import type { McpServer } from "@modelcontextprotocol/server"
+import { ResourceTemplate, type McpServer } from "@modelcontextprotocol/server"
import supermemoryAppHtml from "../../../dist/src/widget/index.html"
import {
APP_RESOURCE_MIME_TYPE,
@@ -6,33 +6,42 @@ import {
} from "../app-metadata"
import {
WIDGET_DESCRIPTION,
- WIDGET_RESOURCE_META,
+ widgetResourceMeta,
} from "../widget-resource-metadata"
-export function registerWidgetResource(server: McpServer) {
+export function registerWidgetResource(
+ server: McpServer,
+ connectOrigin: string,
+) {
+ const resourceMeta = widgetResourceMeta(connectOrigin)
+ const resourceConfig = {
+ mimeType: APP_RESOURCE_MIME_TYPE,
+ description: WIDGET_DESCRIPTION,
+ _meta: resourceMeta,
+ }
+ const readWidgetResource = (uri: string) => ({
+ contents: [
+ {
+ uri,
+ mimeType: APP_RESOURCE_MIME_TYPE,
+ text: supermemoryAppHtml,
+ _meta: resourceMeta,
+ },
+ ],
+ })
+
server.registerResource(
"Supermemory MCP UI",
SUPERMEMORY_RESOURCE_URI,
- // Listing-level metadata: hosts use this when discovering resources
- // before invoking the read callback. Mirrors the read response below
- // so prefetch/connect-time decisions match what the host will get.
- {
- mimeType: APP_RESOURCE_MIME_TYPE,
- description: WIDGET_DESCRIPTION,
- _meta: WIDGET_RESOURCE_META,
- },
- // Read response: per spec, content-item `_meta.ui` takes precedence
- // over the listing-level value. Set both to the same object so behavior
- // is consistent regardless of which path the host inspects.
- async () => ({
- contents: [
- {
- uri: SUPERMEMORY_RESOURCE_URI,
- mimeType: APP_RESOURCE_MIME_TYPE,
- text: supermemoryAppHtml,
- _meta: WIDGET_RESOURCE_META,
- },
- ],
+ resourceConfig,
+ async () => readWidgetResource(SUPERMEMORY_RESOURCE_URI),
+ )
+ server.registerResource(
+ "Supermemory MCP UI compatibility",
+ new ResourceTemplate("ui://supermemory/app-{version}.html", {
+ list: undefined,
}),
+ resourceConfig,
+ async (uri) => readWidgetResource(uri.href),
)
}
diff --git a/apps/mcp/src/server/server.ts b/apps/mcp/src/server/server.ts
index cfeb7a64..432bddd3 100644
--- a/apps/mcp/src/server/server.ts
+++ b/apps/mcp/src/server/server.ts
@@ -21,8 +21,10 @@ import {
resolveContainerTag as resolveSpaceContainerTag,
spaceStateName,
} from "./space"
+import { uploadStateName } from "./space-state"
const DEFAULT_API_URL = "https://api.supermemory.ai"
+const UPLOAD_SESSION_TTL_MS = 2 * 60 * 1000
const SERVER_INSTRUCTIONS =
"Supermemory is the authenticated user's persistent memory and knowledge layer across conversations and spaces. Use these tools whenever the user wants to recall something they may have saved, inspect stored sources or extracted memories, remember or upload new information, check their Supermemory account or access, change their active space, or explore their memory graph, even if they do not mention Supermemory by name. Use the active or account-default space when none is named. Resolve a named space with listSpaces and pass its key to the relevant tool; change the active space only when the user explicitly asks."
@@ -48,6 +50,7 @@ export function createSupermemoryServer(
env: ServerEnv,
actor: ActorContext,
waitUntil: WaitUntil,
+ mcpOrigin: string,
): McpServer {
const server = new McpServer(
{
@@ -68,6 +71,23 @@ export function createSupermemoryServer(
resolveSpaceContainerTag(explicit, getActiveContainerTag)
const resolveContainerTag = async (explicit?: string) =>
(await resolveSelectedContainerTag(explicit)) ?? DEFAULT_PROJECT_ID
+ const createUploadSession = async () => {
+ const uploadId = crypto.randomUUID()
+ const uploadToken = [crypto.randomUUID(), crypto.randomUUID()]
+ .join("")
+ .replaceAll("-", "")
+ const expiresAt = Date.now() + UPLOAD_SESSION_TTL_MS
+ const uploadState = env.SPACE_STATE.getByName(uploadStateName(uploadId))
+ await uploadState.createUploadSession(uploadToken, {
+ bearerToken: actor.bearerToken,
+ expiresAt,
+ })
+ return {
+ uploadUrl: new URL(`/upload/${uploadId}`, mcpOrigin).toString(),
+ uploadToken,
+ expiresAt,
+ }
+ }
const analytics = createPosthogAnalytics(env, actor, waitUntil)
const toolServer = createTrackedToolServer(
server,
@@ -83,6 +103,7 @@ export function createSupermemoryServer(
resolveContainerTag,
getActiveContainerTag,
setActiveContainerTag,
+ createUploadSession,
getClientInfo: clientInfoFromContext,
errorResult,
})
@@ -93,7 +114,7 @@ export function createSupermemoryServer(
() => getClient(),
resolveSelectedContainerTag,
)
- registerWidgetResource(server)
+ registerWidgetResource(server, mcpOrigin)
registerContextPrompt(server, getClient, resolveSelectedContainerTag)
return server
diff --git a/apps/mcp/src/server/space-presentation.test.ts b/apps/mcp/src/server/space-presentation.test.ts
deleted file mode 100644
index c57fe9d1..00000000
--- a/apps/mcp/src/server/space-presentation.test.ts
+++ /dev/null
@@ -1,64 +0,0 @@
-import { describe, expect, it } from "vitest"
-import type { ContainerTag } from "../shared/types"
-import {
- compactDescription,
- formatFactSection,
- formatSpaceRow,
- sortSpaces,
-} from "./space-presentation"
-
-const space = (
- containerTag: string,
- lastActivityAt: string | null,
-): ContainerTag => ({
- id: containerTag,
- name: `Space ${containerTag}`,
- containerTag,
- description: "A compact space description.",
- visibility: "private",
- createdAt: "2026-01-01T00:00:00.000Z",
- updatedAt: "2026-01-01T00:00:00.000Z",
- isExperimental: false,
- isNova: false,
- documentCount: 2,
- memoryCount: 3,
- lastActivityAt,
-})
-
-describe("space presentation", () => {
- it("keeps the active space first, then sorts by activity", () => {
- const sorted = sortSpaces(
- [
- 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",
- )
-
- expect(sorted.map((item) => item.containerTag)).toEqual([
- "active",
- "newer",
- "older",
- ])
- })
-
- it("formats compact rows without internal database IDs", () => {
- const row = formatSpaceRow(
- space("project-key", "2026-07-29T19:44:28.177Z"),
- "project-key",
- )
-
- expect(row).toContain("[project-key] · Active")
- expect(row).toContain("2 documents · 3 memories")
- expect(row).toContain("Last active Jul 29, 2026")
- expect(row).not.toContain('"id"')
- })
-
- it("caps descriptions and fact lists", () => {
- expect(compactDescription("A".repeat(30), 12)).toBe("AAAAAAAAA...")
- expect(
- formatFactSection("Recent Context", ["one", "two", "three"], 2),
- ).toEqual(["## Recent Context", "- one", "- two", "- +1 more"])
- })
-})
diff --git a/apps/mcp/src/server/space-state.ts b/apps/mcp/src/server/space-state.ts
index 138bd656..6caf5200 100644
--- a/apps/mcp/src/server/space-state.ts
+++ b/apps/mcp/src/server/space-state.ts
@@ -2,6 +2,26 @@ import { DurableObject } from "cloudflare:workers"
import { containerTagSchema } from "./container-tag"
const ACTIVE_CONTAINER_TAG_KEY = "activeContainerTag"
+const UPLOAD_SESSION_KEY = "uploadSession"
+
+export interface UploadSession {
+ bearerToken: string
+ expiresAt: number
+}
+
+async function hashUploadToken(token: string): Promise {
+ const digest = await crypto.subtle.digest(
+ "SHA-256",
+ new TextEncoder().encode(token),
+ )
+ return Array.from(new Uint8Array(digest), (byte) =>
+ byte.toString(16).padStart(2, "0"),
+ ).join("")
+}
+
+export function uploadStateName(uploadId: string): string {
+ return `upload:${uploadId}`
+}
export class SpaceState extends DurableObject {
async getActiveContainerTag(): Promise {
@@ -12,4 +32,42 @@ export class SpaceState extends DurableObject {
const validatedTag = containerTagSchema.parse(containerTag)
await this.ctx.storage.put(ACTIVE_CONTAINER_TAG_KEY, validatedTag)
}
+
+ async createUploadSession(
+ token: string,
+ session: UploadSession,
+ ): Promise {
+ await this.ctx.storage.put(UPLOAD_SESSION_KEY, {
+ ...session,
+ tokenHash: await hashUploadToken(token),
+ })
+ await this.ctx.storage.setAlarm(session.expiresAt)
+ }
+
+ async consumeUploadSession(
+ token: string,
+ ): Promise {
+ const tokenHash = await hashUploadToken(token)
+ return this.ctx.storage.transaction(async (transaction) => {
+ const session = await transaction.get<
+ UploadSession & { tokenHash: string }
+ >(UPLOAD_SESSION_KEY)
+ if (!session) return undefined
+ if (session.expiresAt <= Date.now()) {
+ await transaction.delete(UPLOAD_SESSION_KEY)
+ return undefined
+ }
+ if (session.tokenHash !== tokenHash) return undefined
+
+ await transaction.delete(UPLOAD_SESSION_KEY)
+ return {
+ bearerToken: session.bearerToken,
+ expiresAt: session.expiresAt,
+ }
+ })
+ }
+
+ async alarm(): Promise {
+ await this.ctx.storage.delete(UPLOAD_SESSION_KEY)
+ }
}
diff --git a/apps/mcp/src/server/tools/index.ts b/apps/mcp/src/server/tools/index.ts
index 869016c2..6effe28d 100644
--- a/apps/mcp/src/server/tools/index.ts
+++ b/apps/mcp/src/server/tools/index.ts
@@ -6,13 +6,13 @@ import * as listContainerTags from "./list-container-tags"
import * as listDocuments from "./list-documents"
import * as listMemories from "./list-memories"
import * as memoryGraph from "./memory-graph"
+import * as prepareFileUpload from "./prepare-file-upload"
import * as saveMemory from "./save-memory"
import * as searchMemory from "./search-memory"
import * as selectSpace from "./select-space"
import * as setActiveTag from "./set-active-tag"
import type { ToolDeps } from "./types"
import * as uploadFile from "./upload-file"
-import * as uploadFileSubmit from "./upload-file-submit"
import * as whoAmI from "./who-am-i"
export function registerAllTools(deps: ToolDeps) {
@@ -30,5 +30,5 @@ export function registerAllTools(deps: ToolDeps) {
guidedSave.register(deps)
saveMemory.register(deps)
uploadFile.register(deps)
- uploadFileSubmit.register(deps)
+ prepareFileUpload.register(deps)
}
diff --git a/apps/mcp/src/server/tools/memory-graph.ts b/apps/mcp/src/server/tools/memory-graph.ts
index 5b0177b7..173c0ed3 100644
--- a/apps/mcp/src/server/tools/memory-graph.ts
+++ b/apps/mcp/src/server/tools/memory-graph.ts
@@ -1,9 +1,5 @@
import { z } from "zod"
-import {
- graphViewSchema,
- type GraphResultMeta,
- type ViewMessage,
-} from "../../shared/types"
+import { graphViewSchema, type ViewMessage } from "../../shared/types"
import { appResultMeta, appToolMeta } from "../app-metadata"
import { optionalContainerTagSchema } from "../container-tag"
import { READ_ONLY_TOOL_ANNOTATIONS } from "./annotations"
@@ -47,10 +43,6 @@ export function register(deps: ToolDeps) {
truncated: result.documents.length < result.pagination.totalItems,
rendered: true,
}
- const graphMeta: GraphResultMeta = {
- graphData: { documents: result.documents },
- }
-
return {
content: [
textContent(
@@ -58,7 +50,7 @@ export function register(deps: ToolDeps) {
),
],
structuredContent: sc,
- _meta: { ...appResultMeta(viewId), ...graphMeta },
+ _meta: appResultMeta(viewId),
}
} catch (error) {
return deps.errorResult(error)
diff --git a/apps/mcp/src/server/tools/prepare-file-upload.ts b/apps/mcp/src/server/tools/prepare-file-upload.ts
new file mode 100644
index 00000000..835e0cd3
--- /dev/null
+++ b/apps/mcp/src/server/tools/prepare-file-upload.ts
@@ -0,0 +1,30 @@
+import { z } from "zod"
+import { uploadPreparationSchema } from "../../shared/types"
+import { appToolMeta } from "../app-metadata"
+import { ADDITIVE_MEMORY_TOOL_ANNOTATIONS } from "./annotations"
+import { textContent, type ToolDeps } from "./types"
+
+export function register(deps: ToolDeps) {
+ deps.server.registerTool(
+ "prepare-file-upload",
+ {
+ description: "Prepare a direct file upload",
+ inputSchema: z.object({}),
+ outputSchema: uploadPreparationSchema,
+ annotations: ADDITIVE_MEMORY_TOOL_ANNOTATIONS,
+ _meta: appToolMeta(["app"]),
+ },
+ async () => {
+ try {
+ const preparation = await deps.createUploadSession()
+
+ return {
+ content: [textContent("Upload session prepared")],
+ structuredContent: preparation,
+ }
+ } catch (error) {
+ return deps.errorResult(error)
+ }
+ },
+ )
+}
diff --git a/apps/mcp/src/server/tools/types.ts b/apps/mcp/src/server/tools/types.ts
index 0d07b1f5..badbdd74 100644
--- a/apps/mcp/src/server/tools/types.ts
+++ b/apps/mcp/src/server/tools/types.ts
@@ -8,6 +8,12 @@ import type { SessionInfo } from "../../shared/types"
import type { SupermemoryClient } from "../client"
import type { ActorContext } from "../types"
+export interface PreparedUpload {
+ uploadUrl: string
+ uploadToken: string
+ expiresAt: number
+}
+
// Dependencies passed to every tool's register() function.
// Keep this surface small — tools should read this rather than reach into the agent.
export interface ToolDeps {
@@ -18,6 +24,7 @@ export interface ToolDeps {
resolveContainerTag: (explicit?: string) => Promise
getActiveContainerTag: () => Promise
setActiveContainerTag: (containerTag: string) => Promise
+ createUploadSession: () => Promise
getClientInfo: (
context: ServerContext,
) => { name: string; version?: string } | null
diff --git a/apps/mcp/src/server/tools/upload-file-submit.ts b/apps/mcp/src/server/tools/upload-file-submit.ts
deleted file mode 100644
index 3c534427..00000000
--- a/apps/mcp/src/server/tools/upload-file-submit.ts
+++ /dev/null
@@ -1,61 +0,0 @@
-import { z } from "zod"
-import { uploadSuccessViewSchema, type ViewMessage } from "../../shared/types"
-import { appResultMeta, appToolMeta } from "../app-metadata"
-import { containerTagSchema } from "../container-tag"
-import { ADDITIVE_MEMORY_TOOL_ANNOTATIONS } from "./annotations"
-import { textContent, type ToolDeps } from "./types"
-
-export function register(deps: ToolDeps) {
- deps.server.registerTool(
- "upload-file-submit",
- {
- description: "Submit a file upload",
- inputSchema: z.object({
- fileData: z.string().describe("Base64-encoded file content"),
- fileName: z.string(),
- mimeType: z.string(),
- containerTag: containerTagSchema,
- viewId: z.string().uuid().optional(),
- }),
- outputSchema: uploadSuccessViewSchema,
- annotations: ADDITIVE_MEMORY_TOOL_ANNOTATIONS,
- _meta: appToolMeta(["app"]),
- },
- async (args) => {
- try {
- const viewId = args.viewId ?? crypto.randomUUID()
- const binaryString = atob(args.fileData)
- const bytes = new Uint8Array(binaryString.length)
- for (let i = 0; i < binaryString.length; i++) {
- bytes[i] = binaryString.charCodeAt(i)
- }
-
- const client = deps.getClient(args.containerTag)
- const result = await client.uploadFile(
- bytes.buffer,
- args.fileName,
- args.mimeType,
- args.containerTag,
- )
-
- const sc: ViewMessage = {
- view: "upload-success",
- viewId,
- id: result.id,
- fileName: args.fileName,
- containerTag: args.containerTag,
- }
-
- return {
- content: [
- textContent(`File uploaded: ${args.fileName} → ${result.id}`),
- ],
- structuredContent: sc,
- _meta: appResultMeta(viewId),
- }
- } catch (error) {
- return deps.errorResult(error)
- }
- },
- )
-}
diff --git a/apps/mcp/src/server/types.ts b/apps/mcp/src/server/types.ts
index c4bc13d5..fe48f3b2 100644
--- a/apps/mcp/src/server/types.ts
+++ b/apps/mcp/src/server/types.ts
@@ -11,6 +11,7 @@ export interface ServerEnv {
SPACE_STATE: DurableObjectNamespace
API_URL?: string
MCP_RESOURCE?: string
+ MCP_PUBLIC_ORIGIN?: string
ALLOWED_MCP_ORIGIN_HOSTNAMES?: string
POSTHOG_API_KEY?: string
POSTHOG_HOST?: string
diff --git a/apps/mcp/src/server/widget-resource-metadata.ts b/apps/mcp/src/server/widget-resource-metadata.ts
index fdc74f0f..dbf29529 100644
--- a/apps/mcp/src/server/widget-resource-metadata.ts
+++ b/apps/mcp/src/server/widget-resource-metadata.ts
@@ -3,22 +3,30 @@ export const WIDGET_DESCRIPTION =
const WIDGET_DOMAIN = "https://mcp.supermemory.ai"
-export const WIDGET_RESOURCE_UI_META = {
- prefersBorder: true,
- csp: {
- resourceDomains: [
- "https://fonts.googleapis.com",
- "https://fonts.gstatic.com",
- ],
- connectDomains: [
- "https://fonts.googleapis.com",
- "https://fonts.gstatic.com",
- ],
- },
+function widgetResourceUiMeta(connectOrigin = WIDGET_DOMAIN) {
+ return {
+ prefersBorder: true,
+ csp: {
+ resourceDomains: [
+ "https://fonts.googleapis.com",
+ "https://fonts.gstatic.com",
+ ],
+ connectDomains: [
+ "https://fonts.googleapis.com",
+ "https://fonts.gstatic.com",
+ ...new Set([WIDGET_DOMAIN, connectOrigin]),
+ ],
+ },
+ }
}
-export const WIDGET_RESOURCE_META = {
- ui: WIDGET_RESOURCE_UI_META,
- "openai/widgetDescription": WIDGET_DESCRIPTION,
- "openai/widgetDomain": WIDGET_DOMAIN,
+export function widgetResourceMeta(connectOrigin = WIDGET_DOMAIN) {
+ return {
+ ui: widgetResourceUiMeta(connectOrigin),
+ "openai/widgetDescription": WIDGET_DESCRIPTION,
+ "openai/widgetDomain": WIDGET_DOMAIN,
+ }
}
+
+export const WIDGET_RESOURCE_UI_META = widgetResourceUiMeta()
+export const WIDGET_RESOURCE_META = widgetResourceMeta()
diff --git a/apps/mcp/src/shared/types.ts b/apps/mcp/src/shared/types.ts
index 83e61845..b8c4fe35 100644
--- a/apps/mcp/src/shared/types.ts
+++ b/apps/mcp/src/shared/types.ts
@@ -165,12 +165,23 @@ export const uploadSuccessViewSchema = z.object({
containerTag: z.string(),
})
+export const uploadPreparationSchema = z.object({
+ uploadUrl: z.string().url(),
+ uploadToken: z.string().min(1),
+ expiresAt: z.number().int().positive(),
+})
+
+export const uploadResponseSchema = z.object({
+ id: z.string(),
+ status: z.string(),
+})
+
export const graphViewSchema = z.object({
view: z.literal("graph"),
viewId: viewIdSchema,
containerTag: z.string().optional(),
- documents: z.array(documentWithMemoriesSchema).optional(),
- totalCount: z.number().int().nonnegative().optional(),
+ documents: z.array(documentWithMemoriesSchema),
+ totalCount: z.number().int().nonnegative(),
documentCount: z.number().int().nonnegative(),
memoryCount: z.number().int().nonnegative(),
totalDocumentCount: z.number().int().nonnegative(),
@@ -190,12 +201,4 @@ export const viewMessageSchema = z.discriminatedUnion("view", [
export type ViewMessage = z.infer
-export const graphResultMetaSchema = z.looseObject({
- graphData: z.object({
- documents: z.array(documentWithMemoriesSchema),
- }),
-})
-
-export type GraphResultMeta = z.infer
-
export type ViewName = ViewMessage["view"]
diff --git a/apps/mcp/src/widget/App.tsx b/apps/mcp/src/widget/App.tsx
index bd06ee82..750aec70 100644
--- a/apps/mcp/src/widget/App.tsx
+++ b/apps/mcp/src/widget/App.tsx
@@ -1,7 +1,6 @@
-import { type ReactNode, useEffect } from "react"
+import type { ReactNode } from "react"
import type { ViewMessage } from "../shared/types"
import { useApplyHostTheme } from "./hooks/useApplyHostTheme"
-import { useLog } from "./hooks/useLog"
import { useViewState } from "./hooks/useViewState"
import { Confirmation } from "./views/Confirmation"
import { ErrorView } from "./views/Error"
@@ -14,17 +13,8 @@ import { Upload } from "./views/Upload"
export function App() {
useApplyHostTheme()
- const log = useLog()
const { state, setView, setError } = useViewState()
- useEffect(() => {
- if (state.kind === "view") {
- log("info", `[app] view → ${state.message.view}`)
- } else if (state.kind === "error") {
- log("error", `[app] error: ${state.message}`)
- }
- }, [state, log])
-
if (state.kind === "loading") {
return (
@@ -130,13 +120,7 @@ function renderView(
/>
)
case "graph":
- return (
-
- )
+ return
case "confirmation":
return
case "save-success":
diff --git a/apps/mcp/src/widget/ErrorBoundary.tsx b/apps/mcp/src/widget/ErrorBoundary.tsx
index 6dbe70b7..6e046e9c 100644
--- a/apps/mcp/src/widget/ErrorBoundary.tsx
+++ b/apps/mcp/src/widget/ErrorBoundary.tsx
@@ -1,11 +1,5 @@
-import {
- Component,
- type ContextType,
- type ErrorInfo,
- type ReactNode,
-} from "react"
+import { Component, type ReactNode } from "react"
import { Button, Stack } from "./design/ui"
-import { McpAppContext } from "./McpAppProvider"
interface Props {
children: ReactNode
@@ -17,32 +11,11 @@ interface State {
export class ErrorBoundary extends Component {
state: State = { error: null }
- static contextType = McpAppContext
- declare context: ContextType
static getDerivedStateFromError(error: Error): State {
return { error }
}
- componentDidCatch(error: Error, info: ErrorInfo) {
- try {
- const report = this.context?.app?.sendLog({
- level: "error",
- logger: "ErrorBoundary",
- data: `${error.name}: ${error.message}\n${info.componentStack ?? ""}`,
- })
- if (report) {
- void report.catch(() => {
- console.error("[ErrorBoundary]", error, info)
- })
- } else {
- console.error("[ErrorBoundary]", error, info)
- }
- } catch {
- console.error("[ErrorBoundary]", error, info)
- }
- }
-
private handleReload = () => this.setState({ error: null })
render() {
diff --git a/apps/mcp/src/widget/McpAppProvider.tsx b/apps/mcp/src/widget/McpAppProvider.tsx
index 8fa33a93..d7254b23 100644
--- a/apps/mcp/src/widget/McpAppProvider.tsx
+++ b/apps/mcp/src/widget/McpAppProvider.tsx
@@ -30,20 +30,6 @@ export interface McpAppContextValue {
export const McpAppContext = createContext(null)
-function safeLog(
- app: McpApp,
- level: "debug" | "info" | "warning" | "error",
- message: string,
-) {
- try {
- void app.sendLog({ level, data: message }).catch(() => {
- // Host logging is optional.
- })
- } catch {
- // The transport may not be ready yet.
- }
-}
-
function initialViewState(): ViewState {
const checkpoint = loadViewCheckpoint()
return checkpoint
@@ -61,29 +47,21 @@ export function McpAppProvider({ children }: { children: ReactNode }) {
strict: true,
onAppCreated: (createdApp) => {
createdApp.ontoolinput = () => {
- safeLog(createdApp, "info", "[host] ontoolinput")
setState({ kind: "loading" })
}
createdApp.ontoolinputpartial = () => setState({ kind: "loading" })
createdApp.ontoolcancelled = () => {
- safeLog(createdApp, "info", "[host] ontoolcancelled")
setState({ kind: "loading" })
}
createdApp.ontoolresult = (result) => {
const structuredContent = result.structuredContent
const parsedMessage = viewMessageSchema.safeParse(structuredContent)
if (!parsedMessage.success) {
- safeLog(
- createdApp,
- "warning",
- "[host] ontoolresult: invalid structuredContent",
- )
setState({ kind: "raw", structuredContent })
return
}
const message = parsedMessage.data
- safeLog(createdApp, "info", `[host] ontoolresult: view=${message.view}`)
const checkpoint = loadViewCheckpoint(message.viewId)
if (checkpoint) {
setState({ kind: "view", message: checkpoint })
@@ -96,7 +74,6 @@ export function McpAppProvider({ children }: { children: ReactNode }) {
setHostContext(createdApp.getHostContext() ?? next)
}
createdApp.onerror = (nextError: unknown) => {
- safeLog(createdApp, "error", `[host] onerror: ${String(nextError)}`)
setState({ kind: "error", message: String(nextError) })
}
},
diff --git a/apps/mcp/src/widget/hooks/useApp.ts b/apps/mcp/src/widget/hooks/useApp.ts
index 2d8b6de0..876d8dc7 100644
--- a/apps/mcp/src/widget/hooks/useApp.ts
+++ b/apps/mcp/src/widget/hooks/useApp.ts
@@ -101,13 +101,6 @@ export function useApp() {
return performModelHandoff(app, request)
},
- /** Send a structured log line to the host. */
- log(level: "debug" | "info" | "warning" | "error", message: string) {
- return app
- ? app.sendLog({ level, data: message })
- : Promise.resolve(undefined)
- },
-
/** Request the host to switch display mode. */
requestDisplayMode(mode: "inline" | "fullscreen" | "pip") {
return app
diff --git a/apps/mcp/src/widget/hooks/useLog.ts b/apps/mcp/src/widget/hooks/useLog.ts
deleted file mode 100644
index 508e3d1e..00000000
--- a/apps/mcp/src/widget/hooks/useLog.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-import { useCallback } from "react"
-import { useApp } from "./useApp"
-
-/**
- * Convenience wrapper around `app.sendLog` that swallows transport errors —
- * widgets shouldn't crash because the host couldn't accept a log message.
- */
-export function useLog() {
- const { log } = useApp()
- return useCallback(
- (level: "debug" | "info" | "warning" | "error", message: string) => {
- log(level, message).catch(() => {
- /* host may not support logging — ignore */
- })
- },
- [log],
- )
-}
diff --git a/apps/mcp/src/widget/lib/modelHandoff.test.ts b/apps/mcp/src/widget/lib/modelHandoff.test.ts
deleted file mode 100644
index 2fa289f7..00000000
--- a/apps/mcp/src/widget/lib/modelHandoff.test.ts
+++ /dev/null
@@ -1,107 +0,0 @@
-import { describe, expect, it, vi } from "vitest"
-import { handoffToModel } from "./modelHandoff"
-
-function createApp(overrides?: {
- updateModelContext?: () => Promise
- sendMessage?: () => Promise<{ isError?: boolean }>
-}) {
- return {
- updateModelContext:
- overrides?.updateModelContext ?? vi.fn(async () => ({})),
- sendMessage: overrides?.sendMessage ?? vi.fn(async () => ({})),
- }
-}
-
-const request = {
- context: "Detailed state",
- message: "Continue from the widget action",
- structuredContent: { action: "saved" },
-}
-
-describe("handoffToModel", () => {
- it("updates context before sending the portable conversation message", async () => {
- const order: string[] = []
- const app = createApp({
- updateModelContext: vi.fn(async () => {
- order.push("context")
- }),
- sendMessage: vi.fn(async () => {
- order.push("message")
- return {}
- }),
- })
-
- const result = await handoffToModel(app, request, undefined)
-
- expect(order).toEqual(["context", "message"])
- expect(result).toEqual({
- ok: true,
- contextUpdate: { ok: true },
- conversationMessage: { ok: true },
- })
- expect(app.updateModelContext).toHaveBeenCalledWith({
- content: [{ type: "text", text: request.context }],
- structuredContent: request.structuredContent,
- })
- })
-
- it("prefers ChatGPT's follow-up helper when available", async () => {
- const app = createApp()
- const sendFollowUpMessage = vi.fn(async () => undefined)
-
- const result = await handoffToModel(app, request, {
- sendFollowUpMessage,
- })
-
- expect(result.conversationMessage).toEqual({ ok: true })
- expect(sendFollowUpMessage).toHaveBeenCalledWith({
- prompt: request.message,
- scrollToBottom: true,
- })
- expect(app.sendMessage).not.toHaveBeenCalled()
- })
-
- it("falls back to the portable message when ChatGPT's helper fails", async () => {
- const app = createApp()
-
- const result = await handoffToModel(app, request, {
- sendFollowUpMessage: vi.fn(async () => {
- throw new Error("unavailable")
- }),
- })
-
- expect(result.conversationMessage).toEqual({ ok: true })
- expect(app.sendMessage).toHaveBeenCalledOnce()
- })
-
- it("still sends the conversation message when context publication fails", async () => {
- const app = createApp({
- updateModelContext: vi.fn(async () => {
- throw new Error("unsupported")
- }),
- })
-
- const result = await handoffToModel(app, request, undefined)
-
- expect(result.ok).toBe(true)
- expect(result.contextUpdate).toMatchObject({
- ok: false,
- error: "Error: unsupported",
- })
- expect(app.sendMessage).toHaveBeenCalledOnce()
- })
-
- it("reports a rejected conversation message as the failed handoff", async () => {
- const app = createApp({
- sendMessage: vi.fn(async () => ({ isError: true })),
- })
-
- const result = await handoffToModel(app, request, undefined)
-
- expect(result.ok).toBe(false)
- expect(result.conversationMessage).toEqual({
- ok: false,
- error: "Host rejected the MCP Apps message",
- })
- })
-})
diff --git a/apps/mcp/src/widget/lib/readFileAsBase64.ts b/apps/mcp/src/widget/lib/readFileAsBase64.ts
deleted file mode 100644
index 3561e57c..00000000
--- a/apps/mcp/src/widget/lib/readFileAsBase64.ts
+++ /dev/null
@@ -1,19 +0,0 @@
-/**
- * Reads a File as a base64-encoded string (without the `data:...;base64,` prefix).
- */
-export function readFileAsBase64(file: File): Promise {
- return new Promise((resolve, reject) => {
- const reader = new FileReader()
- reader.onload = () => {
- if (typeof reader.result !== "string") {
- reject(new Error("Unable to read file as base64"))
- return
- }
- const result = reader.result
- const comma = result.indexOf(",")
- resolve(comma >= 0 ? result.slice(comma + 1) : result)
- }
- reader.onerror = () => reject(reader.error ?? new Error("read failed"))
- reader.readAsDataURL(file)
- })
-}
diff --git a/apps/mcp/src/widget/lib/viewCheckpoint.test.ts b/apps/mcp/src/widget/lib/viewCheckpoint.test.ts
deleted file mode 100644
index 5109601f..00000000
--- a/apps/mcp/src/widget/lib/viewCheckpoint.test.ts
+++ /dev/null
@@ -1,105 +0,0 @@
-import { afterEach, describe, expect, it, vi } from "vitest"
-import type { ViewMessage } from "../../shared/types"
-import { loadViewCheckpoint, saveViewCheckpoint } from "./viewCheckpoint"
-
-function createStorage() {
- const values = new Map()
- return {
- getItem: vi.fn((key: string) => values.get(key) ?? null),
- setItem: vi.fn((key: string, value: string) => {
- values.set(key, value)
- }),
- removeItem: vi.fn((key: string) => {
- values.delete(key)
- }),
- clear: vi.fn(() => values.clear()),
- key: vi.fn(() => null),
- get length() {
- return values.size
- },
- }
-}
-
-afterEach(() => {
- vi.unstubAllGlobals()
-})
-
-describe("view checkpoints", () => {
- it("restores a completed view from localStorage by stable view id", () => {
- const storage = createStorage()
- vi.stubGlobal("localStorage", storage)
- const view: ViewMessage = {
- view: "save-success",
- viewId: "80fa74b9-8347-45f7-a2b5-d7a3d5d67616",
- id: "memory-123",
- containerTag: "model_test",
- }
-
- saveViewCheckpoint(view)
-
- expect(loadViewCheckpoint(view.viewId)).toEqual(view)
- })
-
- it("does not persist non-terminal form views", () => {
- const storage = createStorage()
- vi.stubGlobal("localStorage", storage)
- const view: ViewMessage = {
- view: "save",
- viewId: "80fa74b9-8347-45f7-a2b5-d7a3d5d67616",
- writableTags: ["model_test"],
- }
-
- saveViewCheckpoint(view)
-
- expect(storage.setItem).not.toHaveBeenCalled()
- expect(loadViewCheckpoint(view.viewId)).toBeNull()
- })
-
- it("mirrors compact state into ChatGPT widget state", () => {
- const storage = createStorage()
- const setWidgetState = vi.fn()
- vi.stubGlobal("localStorage", storage)
- vi.stubGlobal("window", {
- openai: {
- widgetState: {
- privateContent: { existing: true },
- },
- setWidgetState,
- },
- })
- const view: ViewMessage = {
- view: "confirmation",
- viewId: "80fa74b9-8347-45f7-a2b5-d7a3d5d67616",
- containerTag: "model_test",
- }
-
- saveViewCheckpoint(view)
-
- expect(setWidgetState).toHaveBeenCalledWith({
- modelContent: 'Supermemory active space is now "model_test".',
- privateContent: {
- existing: true,
- supermemoryView: view,
- },
- })
- })
-
- it("can restore from ChatGPT widget state before a tool result is replayed", () => {
- const view: ViewMessage = {
- view: "upload-success",
- viewId: "80fa74b9-8347-45f7-a2b5-d7a3d5d67616",
- id: "document-123",
- fileName: "notes.txt",
- containerTag: "model_test",
- }
- vi.stubGlobal("window", {
- openai: {
- widgetState: {
- privateContent: { supermemoryView: view },
- },
- },
- })
-
- expect(loadViewCheckpoint()).toEqual(view)
- })
-})
diff --git a/apps/mcp/src/widget/studio/Studio.tsx b/apps/mcp/src/widget/studio/Studio.tsx
index f44eec33..54d83fb9 100644
--- a/apps/mcp/src/widget/studio/Studio.tsx
+++ b/apps/mcp/src/widget/studio/Studio.tsx
@@ -425,8 +425,8 @@ export function Studio() {
diff --git a/apps/mcp/src/widget/views/Graph.tsx b/apps/mcp/src/widget/views/Graph.tsx
index 5b61c5f3..1c9631be 100644
--- a/apps/mcp/src/widget/views/Graph.tsx
+++ b/apps/mcp/src/widget/views/Graph.tsx
@@ -6,18 +6,14 @@ import {
MemoryGraph,
} from "@supermemory/memory-graph"
import { useCallback, useEffect, useMemo, useRef, useState } from "react"
-import {
- documentsApiResponseSchema,
- type DocumentMemoryEntry,
- type DocumentWithMemories,
+import type {
+ DocumentMemoryEntry,
+ DocumentWithMemories,
} from "../../shared/types"
import { cn } from "../design/lib/cn"
import { useApp } from "../hooks/useApp"
import { useHostContext } from "../hooks/useHostContext"
-import { useLog } from "../hooks/useLog"
import { ArrowsIn, ArrowsOut } from "../lib/icons"
-import { ErrorView } from "./Error"
-import { Loading } from "./Loading"
// GraphThemeColors key → the --graph-* CSS variable it resolves from. Same
// mapping as the package's internal useGraphTheme, but we drive it ourselves
@@ -84,9 +80,8 @@ function useGraphColors(theme: string): GraphThemeColors {
}
interface Props {
- containerTag?: string
- initialDocuments?: DocumentWithMemories[]
- initialTotalCount: number
+ documents: DocumentWithMemories[]
+ totalCount: number
}
// Map the widget's API shape (DocumentWithMemories) into the package's
@@ -127,48 +122,12 @@ function toGraphDocument(doc: DocumentWithMemories): GraphApiDocument {
}
}
-export function Graph({
- containerTag,
- initialDocuments,
- initialTotalCount,
-}: Props) {
- const { callTool, isConnected, requestDisplayMode } = useApp()
+export function Graph({ documents, totalCount }: Props) {
+ const { requestDisplayMode } = useApp()
const ctx = useHostContext()
- const log = useLog()
- const [documents, setDocuments] = useState(initialDocuments)
- const [totalCount, setTotalCount] = useState(initialTotalCount)
- const [loadError, setLoadError] = useState(null)
-
- useEffect(() => {
- if (!isConnected) return
- let active = true
- void callTool(
- "fetch-graph-data",
- {
- ...(containerTag ? { containerTag } : {}),
- page: 1,
- limit: 200,
- },
- documentsApiResponseSchema,
- ).then((result) => {
- if (!active) return
- if (!result.ok || !result.data) {
- if (!initialDocuments) {
- setLoadError(result.error ?? "Failed to load graph data")
- }
- return
- }
- setDocuments(result.data.documents)
- setTotalCount(result.data.pagination.totalItems)
- setLoadError(null)
- })
- return () => {
- active = false
- }
- }, [callTool, containerTag, initialDocuments, isConnected])
const graphDocuments = useMemo(
- () => (documents ?? []).map(toGraphDocument),
+ () => documents.map(toGraphDocument),
[documents],
)
@@ -206,15 +165,11 @@ export function Graph({
const toggleFullscreen = useCallback(async () => {
const next = mode === "fullscreen" ? "inline" : "fullscreen"
- log("info", `[graph] fullscreen toggle: ${mode} → ${next}`)
setMode(next) // optimistic — button + container update immediately
try {
- const result = await requestDisplayMode(next)
- log("info", `[graph] requestDisplayMode result: ${result?.mode ?? "?"}`)
- } catch (err) {
- log("error", `[graph] requestDisplayMode failed: ${err}`)
- }
- }, [mode, requestDisplayMode, log])
+ await requestDisplayMode(next)
+ } catch {}
+ }, [mode, requestDisplayMode])
// ESC exits fullscreen — matches Excalidraw and host-page UX.
useEffect(() => {
@@ -229,9 +184,6 @@ export function Graph({
return () => document.removeEventListener("keydown", handler)
}, [mode, toggleFullscreen])
- if (loadError) return
- if (!documents) return
-
return (
(null)
const [query, setQuery] = useState("")
@@ -48,7 +46,6 @@ export function Picker({
}, [containerTags, query])
const handleSelect = async (containerTag: string) => {
- log("info", `[picker] select: ${containerTag}`)
setPending(containerTag)
const result = await callTool(
"set-active-tag",
@@ -60,12 +57,11 @@ 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 space")
return
}
onAdvance(result.data)
- const handoff = await handoffToModel({
+ await handoffToModel({
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: {
@@ -75,18 +71,6 @@ export function Picker({
},
},
})
- if (!handoff.contextUpdate.ok) {
- log(
- "warning",
- `[picker] model context update failed: ${handoff.contextUpdate.error}`,
- )
- }
- if (!handoff.conversationMessage.ok) {
- log(
- "warning",
- `[picker] agent handoff failed: ${handoff.conversationMessage.error}`,
- )
- }
}
const count = containerTags.length
diff --git a/apps/mcp/src/widget/views/Save.tsx b/apps/mcp/src/widget/views/Save.tsx
index 0400291b..355f0036 100644
--- a/apps/mcp/src/widget/views/Save.tsx
+++ b/apps/mcp/src/widget/views/Save.tsx
@@ -10,7 +10,6 @@ import {
SpaceSelect,
} from "../design/ui"
import { useApp } from "../hooks/useApp"
-import { useLog } from "../hooks/useLog"
import { formatTagLabel } from "../lib/formatTag"
interface Props {
@@ -31,7 +30,6 @@ export function Save({
viewId,
}: Props) {
const { callTool, handoffToModel } = useApp()
- const log = useLog()
const [content, setContent] = useState(prefill ?? "")
const [selectedTag, setSelectedTag] = useState
(
activeTag ?? writableTags[0] ?? null,
@@ -59,7 +57,6 @@ export function Save({
const handleSave = async () => {
if (!canSave || !selectedTag) return
- log("info", `[save] submit (${trimmed.length} chars → ${selectedTag})`)
setSaving(true)
const result = await callTool(
"save-memory",
@@ -72,14 +69,13 @@ export function Save({
)
setSaving(false)
if (!result.ok || !result.data) {
- log("error", `[save] failed: ${result.error}`)
onError(result.error ?? "Failed to save memory")
return
}
const memoryId =
result.data.view === "save-success" ? result.data.id : undefined
onAdvance(result.data)
- const handoff = await handoffToModel({
+ await handoffToModel({
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: {
@@ -91,18 +87,6 @@ export function Save({
},
},
})
- if (!handoff.contextUpdate.ok) {
- log(
- "warning",
- `[save] model context update failed: ${handoff.contextUpdate.error}`,
- )
- }
- if (!handoff.conversationMessage.ok) {
- log(
- "warning",
- `[save] agent handoff failed: ${handoff.conversationMessage.error}`,
- )
- }
}
return (
diff --git a/apps/mcp/src/widget/views/Upload.tsx b/apps/mcp/src/widget/views/Upload.tsx
index e69e8f7f..e388aa51 100644
--- a/apps/mcp/src/widget/views/Upload.tsx
+++ b/apps/mcp/src/widget/views/Upload.tsx
@@ -1,5 +1,9 @@
import { useMemo, useState } from "react"
-import { viewMessageSchema, type ViewMessage } from "../../shared/types"
+import {
+ uploadPreparationSchema,
+ uploadResponseSchema,
+ type ViewMessage,
+} from "../../shared/types"
import {
ActionGroup,
Button,
@@ -10,10 +14,8 @@ import {
SpaceSelect,
} from "../design/ui"
import { useApp } from "../hooks/useApp"
-import { useLog } from "../hooks/useLog"
import { formatTagLabel } from "../lib/formatTag"
import { FileText, X } from "../lib/icons"
-import { readFileAsBase64 } from "../lib/readFileAsBase64"
interface Props {
activeTag?: string | null
@@ -40,7 +42,6 @@ export function Upload({
viewId,
}: Props) {
const { callTool, handoffToModel } = useApp()
- const log = useLog()
const [file, setFile] = useState(null)
const [selectedTag, setSelectedTag] = useState(
activeTag ?? writableTags[0] ?? null,
@@ -61,55 +62,67 @@ export function Upload({
const handleUpload = async () => {
if (!file || !selectedTag) return
- log("info", `[upload] submit ${file.name} (${file.size}B → ${selectedTag})`)
setUploading(true)
try {
- const fileData = await readFileAsBase64(file)
- const result = await callTool(
- "upload-file-submit",
- {
- fileData,
- fileName: file.name,
- mimeType: file.type,
- containerTag: selectedTag,
- viewId,
- },
- viewMessageSchema,
+ const preparation = await callTool(
+ "prepare-file-upload",
+ {},
+ uploadPreparationSchema,
)
- if (!result.ok || !result.data) {
- log("error", `[upload] failed: ${result.error}`)
- onError(result.error ?? "Upload failed")
+ if (!preparation.ok || !preparation.data) {
+ onError(preparation.error ?? "Unable to prepare upload")
return
}
- const documentId =
- 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 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.`,
+
+ const formData = new FormData()
+ formData.append("file", file, file.name)
+ formData.append("containerTag", selectedTag)
+ formData.append(
+ "metadata",
+ JSON.stringify({ sm_source: "supermemory-mcp" }),
+ )
+
+ const response = await fetch(preparation.data.uploadUrl, {
+ method: "POST",
+ headers: {
+ Authorization: `Bearer ${preparation.data.uploadToken}`,
+ },
+ body: formData,
+ })
+ if (!response.ok) {
+ const message =
+ (await response.text()) || `Upload failed (${response.status})`
+ onError(message)
+ return
+ }
+
+ const uploaded = uploadResponseSchema.safeParse(await response.json())
+ if (!uploaded.success) {
+ onError("Upload returned an invalid response")
+ return
+ }
+
+ const result: ViewMessage = {
+ view: "upload-success",
+ viewId,
+ id: uploaded.data.id,
+ fileName: file.name,
+ containerTag: selectedTag,
+ }
+ onAdvance(result)
+ await handoffToModel({
+ context: `Supermemory widget action completed. "${file.name}" was uploaded to space "${selectedTag}" with document ID "${uploaded.data.id}". It is already uploaded; do not upload it again.`,
+ message: `I used the Supermemory widget to upload "${file.name}" to space "${selectedTag}" (document ID: ${uploaded.data.id}). The file is already uploaded; do not upload it again.`,
structuredContent: {
supermemory: {
action: "file-uploaded",
activeSpace: selectedTag,
- documentId,
+ documentId: uploaded.data.id,
fileName: file.name,
},
},
})
- if (!handoff.contextUpdate.ok) {
- log(
- "warning",
- `[upload] model context update failed: ${handoff.contextUpdate.error}`,
- )
- }
- if (!handoff.conversationMessage.ok) {
- log(
- "warning",
- `[upload] agent handoff failed: ${handoff.conversationMessage.error}`,
- )
- }
} catch (err) {
- log("error", `[upload] threw: ${err}`)
onError(String(err))
} finally {
setUploading(false)
From 4d86b728dd13b63917b65a35bfc5b0bdf2305f00 Mon Sep 17 00:00:00 2001
From: Prasanna721 <106952318+Prasanna721@users.noreply.github.com>
Date: Tue, 4 Aug 2026 19:58:49 +0000
Subject: [PATCH 03/70] update supermemory mcp docs (#1408)
Updates the Supermemory MCP docs for the revamped tool, space, widget, and OAuth flows.
- adds a screenshot-backed ChatGPT Web setup guide with light and dark variants
- refreshes the overview, setup, tools, spaces, and widget docs
- keeps manual JSON configuration in a dropdown
Tested all four local MCP docs routes and image references.
for mcp image light mode -> dark mode images and dark mode has light mode (since the background was blending in i made this way to refocus user attention on the screenshots)
---
apps/docs/docs.json | 5 +-
.../chatgpt-web/step-1-settings-dark.png | Bin 0 -> 82420 bytes
.../chatgpt-web/step-1-settings-light.png | Bin 0 -> 77329 bytes
.../chatgpt-web/step-10-authorize.png | Bin 0 -> 203383 bytes
.../chatgpt-web/step-11-connected-dark.png | Bin 0 -> 199299 bytes
.../chatgpt-web/step-11-connected-light.png | Bin 0 -> 188633 bytes
.../chatgpt-web/step-2-plugins-dark.png | Bin 0 -> 231711 bytes
.../chatgpt-web/step-2-plugins-light.png | Bin 0 -> 258480 bytes
.../step-3-plugins-bottom-dark.png | Bin 0 -> 210478 bytes
.../step-3-plugins-bottom-light.png | Bin 0 -> 230473 bytes
.../step-4-developer-mode-on-dark.png | Bin 0 -> 41611 bytes
.../step-4-developer-mode-on-light.png | Bin 0 -> 41678 bytes
.../step-4-developer-mode-page-dark.png | Bin 0 -> 296887 bytes
.../step-4-developer-mode-page-light.png | Bin 0 -> 317666 bytes
.../step-6-plugin-directory-dark.png | Bin 0 -> 104093 bytes
.../step-6-plugin-directory-light.png | Bin 0 -> 114909 bytes
.../chatgpt-web/step-7-details-dark.png | Bin 0 -> 122367 bytes
.../chatgpt-web/step-7-details-light.png | Bin 0 -> 112089 bytes
.../chatgpt-web/step-8-confirm-dark.png | Bin 0 -> 129589 bytes
.../chatgpt-web/step-8-confirm-light.png | Bin 0 -> 92447 bytes
.../chatgpt-web/step-9-organization.png | Bin 0 -> 173142 bytes
.../chatgpt-web/step-9-sign-in-dark.png | Bin 0 -> 705314 bytes
.../chatgpt-web/step-9-sign-in-light.png | Bin 0 -> 683221 bytes
.../widgets/guided-save-dark.png | Bin 0 -> 173849 bytes
.../widgets/guided-save-draft.png | Bin 0 -> 185065 bytes
.../widgets/memory-graph-dark.png | Bin 0 -> 375543 bytes
.../supermemory-mcp/widgets/memory-graph.png | Bin 0 -> 392050 bytes
.../widgets/select-space-dark.png | Bin 0 -> 200718 bytes
.../supermemory-mcp/widgets/select-space.png | Bin 0 -> 211593 bytes
.../widgets/upload-file-dark.png | Bin 0 -> 178118 bytes
.../supermemory-mcp/widgets/upload-file.png | Bin 0 -> 184516 bytes
apps/docs/supermemory-mcp/chatgpt-web.mdx | 199 ++++++++++++++++
apps/docs/supermemory-mcp/claude-desktop.mdx | 2 +-
apps/docs/supermemory-mcp/mcp.mdx | 218 ++++++++++--------
apps/docs/supermemory-mcp/setup.mdx | 62 ++---
35 files changed, 359 insertions(+), 127 deletions(-)
create mode 100644 apps/docs/images/supermemory-mcp/chatgpt-web/step-1-settings-dark.png
create mode 100644 apps/docs/images/supermemory-mcp/chatgpt-web/step-1-settings-light.png
create mode 100644 apps/docs/images/supermemory-mcp/chatgpt-web/step-10-authorize.png
create mode 100644 apps/docs/images/supermemory-mcp/chatgpt-web/step-11-connected-dark.png
create mode 100644 apps/docs/images/supermemory-mcp/chatgpt-web/step-11-connected-light.png
create mode 100644 apps/docs/images/supermemory-mcp/chatgpt-web/step-2-plugins-dark.png
create mode 100644 apps/docs/images/supermemory-mcp/chatgpt-web/step-2-plugins-light.png
create mode 100644 apps/docs/images/supermemory-mcp/chatgpt-web/step-3-plugins-bottom-dark.png
create mode 100644 apps/docs/images/supermemory-mcp/chatgpt-web/step-3-plugins-bottom-light.png
create mode 100644 apps/docs/images/supermemory-mcp/chatgpt-web/step-4-developer-mode-on-dark.png
create mode 100644 apps/docs/images/supermemory-mcp/chatgpt-web/step-4-developer-mode-on-light.png
create mode 100644 apps/docs/images/supermemory-mcp/chatgpt-web/step-4-developer-mode-page-dark.png
create mode 100644 apps/docs/images/supermemory-mcp/chatgpt-web/step-4-developer-mode-page-light.png
create mode 100644 apps/docs/images/supermemory-mcp/chatgpt-web/step-6-plugin-directory-dark.png
create mode 100644 apps/docs/images/supermemory-mcp/chatgpt-web/step-6-plugin-directory-light.png
create mode 100644 apps/docs/images/supermemory-mcp/chatgpt-web/step-7-details-dark.png
create mode 100644 apps/docs/images/supermemory-mcp/chatgpt-web/step-7-details-light.png
create mode 100644 apps/docs/images/supermemory-mcp/chatgpt-web/step-8-confirm-dark.png
create mode 100644 apps/docs/images/supermemory-mcp/chatgpt-web/step-8-confirm-light.png
create mode 100644 apps/docs/images/supermemory-mcp/chatgpt-web/step-9-organization.png
create mode 100644 apps/docs/images/supermemory-mcp/chatgpt-web/step-9-sign-in-dark.png
create mode 100644 apps/docs/images/supermemory-mcp/chatgpt-web/step-9-sign-in-light.png
create mode 100644 apps/docs/images/supermemory-mcp/widgets/guided-save-dark.png
create mode 100644 apps/docs/images/supermemory-mcp/widgets/guided-save-draft.png
create mode 100644 apps/docs/images/supermemory-mcp/widgets/memory-graph-dark.png
create mode 100644 apps/docs/images/supermemory-mcp/widgets/memory-graph.png
create mode 100644 apps/docs/images/supermemory-mcp/widgets/select-space-dark.png
create mode 100644 apps/docs/images/supermemory-mcp/widgets/select-space.png
create mode 100644 apps/docs/images/supermemory-mcp/widgets/upload-file-dark.png
create mode 100644 apps/docs/images/supermemory-mcp/widgets/upload-file.png
create mode 100644 apps/docs/supermemory-mcp/chatgpt-web.mdx
diff --git a/apps/docs/docs.json b/apps/docs/docs.json
index 63c0af6b..9400ae9f 100644
--- a/apps/docs/docs.json
+++ b/apps/docs/docs.json
@@ -370,7 +370,10 @@
{
"group": "Setups",
"icon": "layers",
- "pages": ["supermemory-mcp/claude-desktop"]
+ "pages": [
+ "supermemory-mcp/chatgpt-web",
+ "supermemory-mcp/claude-desktop"
+ ]
}
]
},
diff --git a/apps/docs/images/supermemory-mcp/chatgpt-web/step-1-settings-dark.png b/apps/docs/images/supermemory-mcp/chatgpt-web/step-1-settings-dark.png
new file mode 100644
index 0000000000000000000000000000000000000000..8cc7fac237e7e44938f591aa0815bbb8f9eb4437
GIT binary patch
literal 82420
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