diff --git a/apps/mcp/README.md b/apps/mcp/README.md index 26476920..d7f4e4e7 100644 --- a/apps/mcp/README.md +++ b/apps/mcp/README.md @@ -55,9 +55,22 @@ The client discovers the OAuth authorization server through | `list_memories` | List extracted memory entries and their source document IDs | | `list_spaces` | List spaces visible to the authenticated account | | `who_am_i` | Return identity, access, and active-space context | -| `add_memory` | Save a memory (legacy `action: "forget"` remains supported) | +| `add_memory` | Accept a document for asynchronous memory extraction (legacy `action: "forget"` remains supported) | | `forget_memory` | Forget a memory matching the supplied content | +### Document status + +`list_documents` and `get_document` return two status fields: + +| Field | Value | Meaning | +| --- | --- | --- | +| `status` | `done` | Content is processed and readable | +| `dreamingStatus` | `dreaming` | Memory extraction is pending, running, or retrying | +| `dreamingStatus` | `done` | Memory extraction settled, possibly with no memories | + +`dreamingStatus` appears only when the API returns it. Neither `done` means +memories are searchable yet. + ### MCP App launchers | Tool | Purpose | diff --git a/apps/mcp/e2e/memory.test.ts b/apps/mcp/e2e/memory.test.ts index c0fef869..8ae38d70 100644 --- a/apps/mcp/e2e/memory.test.ts +++ b/apps/mcp/e2e/memory.test.ts @@ -37,7 +37,7 @@ describe.skipIf(!OAUTH_CREDENTIALS_AVAILABLE)("MCP — memory behaviors", () => action: "save", }) expect(save.isError).toBeFalsy() - expect(textOf(save)).toMatch(/Memory saved/i) + expect(textOf(save)).toMatch(/Document accepted/i) const found = await recallUntil(s.client, "test fruit dragonfruit", marker) expect(found, `recall never returned marker ${marker}`).not.toBeNull() diff --git a/apps/mcp/src/server/client/index.ts b/apps/mcp/src/server/client/index.ts index 4d2a2638..664fa93b 100644 --- a/apps/mcp/src/server/client/index.ts +++ b/apps/mcp/src/server/client/index.ts @@ -35,18 +35,23 @@ export type { NamespaceInfo, } +type DreamingStatus = "dreaming" | "done" + export interface DocumentSummary { id: string title: string | null type: string status: string + dreamingStatus?: DreamingStatus createdAt: string updatedAt: string summary: string | null url: string | null } -export type DocumentDetails = GetDocumentsResponse +export type DocumentDetails = GetDocumentsResponse & { + system: { dreamingStatus?: DreamingStatus } +} export interface DocumentsListResponse { documents: DocumentSummary[] @@ -104,13 +109,16 @@ function mapSearchResults(results: SearchResponseResultsItem[]): Memory[] { } function mapDocumentSummary( - document: ListResponseDocumentsItem, + document: ListResponseDocumentsItem & { + system: { dreamingStatus?: DreamingStatus } + }, ): DocumentSummary { return { id: document.id, title: document.title, type: document.type, status: document.system.status, + dreamingStatus: document.system.dreamingStatus, createdAt: document.system.createdAt, updatedAt: document.system.updatedAt, summary: document.summary, diff --git a/apps/mcp/src/server/format.ts b/apps/mcp/src/server/format.ts index d177ac68..f229bbf1 100644 --- a/apps/mcp/src/server/format.ts +++ b/apps/mcp/src/server/format.ts @@ -18,6 +18,10 @@ function day(value: string | null | undefined): string { return value?.slice(0, 10) ?? "" } +function memoryExtractionLabel(status: "dreaming" | "done"): string { + return status === "dreaming" ? "in progress" : "done" +} + function paginationSummary( currentPage: number, totalPages: number, @@ -43,6 +47,11 @@ export function formatDocumentsList(response: DocumentsListResponse): string { const lines = [ `- [${document.id}] "${title}" (${document.type}, ${document.status}, ${day(document.createdAt)})`, ] + if (document.dreamingStatus) { + lines.push( + ` Memory extraction: ${memoryExtractionLabel(document.dreamingStatus)}`, + ) + } if (document.summary?.trim()) { lines.push(` Summary: ${compactText(document.summary)}`) } @@ -148,6 +157,11 @@ export function formatDocument(document: DocumentDetails): string { `Document ID: ${document.id}`, `Type: ${document.type}`, `Status: ${document.system.status}`, + ...(document.system.dreamingStatus + ? [ + `Memory extraction: ${memoryExtractionLabel(document.system.dreamingStatus)}`, + ] + : []), `Created: ${document.system.createdAt}`, `Updated: ${document.system.updatedAt}`, ] diff --git a/apps/mcp/src/server/tools/add-memory.ts b/apps/mcp/src/server/tools/add-memory.ts index 97bdd6da..b2b6b995 100644 --- a/apps/mcp/src/server/tools/add-memory.ts +++ b/apps/mcp/src/server/tools/add-memory.ts @@ -31,7 +31,7 @@ export function register(deps: ToolDeps) { "add_memory", { description: - "Save a memory in the user's ACTIVE space. The target space is the one the user selected via select-space; pass namespace only to override it. To remove outdated information, use the separately exposed forget_memory tool. Legacy action='forget' remains supported for existing clients.", + "Save content as a document in the user's ACTIVE space. Memories are extracted from it in the background, so they may not be searchable right away. The target space is the one the user selected via select-space; pass namespace only to override it. To remove outdated information, use the separately exposed forget_memory tool. Legacy action='forget' remains supported for existing clients.", inputSchema, outputSchema: addMemoryOutputSchema, annotations: MEMORY_TOOL_ANNOTATIONS, @@ -43,7 +43,7 @@ export function register(deps: ToolDeps) { const client = deps.getClient(namespace) const result = await client.createMemory(args.content) - const message = `Memory saved (ID: ${result.id}, space: ${result.namespace})` + const message = `Document accepted (ID: ${result.id}, space: ${result.namespace}). Memories are extracted in the background and can take a moment to show up in search.` const structuredContent: AddMemoryOutput = withLegacyNamespace({ action: "save", success: true, diff --git a/apps/mcp/src/server/tools/get-document.ts b/apps/mcp/src/server/tools/get-document.ts index f68c13c8..5872de3d 100644 --- a/apps/mcp/src/server/tools/get-document.ts +++ b/apps/mcp/src/server/tools/get-document.ts @@ -31,7 +31,7 @@ export function register(deps: ToolDeps) { { title: "Get Document", description: - "Read one stored document by ID from one space, including its summary and available content. Get document IDs from list_documents or from the document shown on a search_memory result. When the document is in a named space, resolve it with list_spaces and pass namespace; otherwise use the active space.", + "Read one stored document by ID from one space, including its summary, available content, and memory extraction status (dreamingStatus) when available. Get document IDs from list_documents or from the document shown on a search_memory result. When the document is in a named space, resolve it with list_spaces and pass namespace; otherwise use the active space.", inputSchema, outputSchema: getDocumentOutputSchema, annotations: READ_ONLY_TOOL_ANNOTATIONS, @@ -49,6 +49,7 @@ export function register(deps: ToolDeps) { title: document.title, type: document.type, status: document.system.status, + dreamingStatus: document.system.dreamingStatus, createdAt: document.system.createdAt, updatedAt: document.system.updatedAt, summary: document.summary, diff --git a/apps/mcp/src/server/tools/list-documents.ts b/apps/mcp/src/server/tools/list-documents.ts index c886f364..bc8d2fdf 100644 --- a/apps/mcp/src/server/tools/list-documents.ts +++ b/apps/mcp/src/server/tools/list-documents.ts @@ -35,7 +35,7 @@ export function register(deps: ToolDeps) { { title: "List Documents", description: - "List documents in one space with their IDs, titles, types, processing status, dates, and summaries. This does not return full document content; use get_document with an ID from this result to read one document. When the user names a space, resolve it with list_spaces and pass namespace; otherwise use the active space.", + "List documents in one space with their IDs, titles, types, processing status, memory extraction status (dreamingStatus) when available, dates, and summaries. This does not return full document content; use get_document with an ID from this result to read one document. When the user names a space, resolve it with list_spaces and pass namespace; otherwise use the active space.", inputSchema, outputSchema: listDocumentsOutputSchema, annotations: READ_ONLY_TOOL_ANNOTATIONS, @@ -54,6 +54,7 @@ export function register(deps: ToolDeps) { title: document.title, type: document.type, status: document.status, + dreamingStatus: document.dreamingStatus, createdAt: document.createdAt, updatedAt: document.updatedAt, summary: document.summary, diff --git a/apps/mcp/src/server/tools/output-schemas.ts b/apps/mcp/src/server/tools/output-schemas.ts index 5a75e8c7..29f2ebd0 100644 --- a/apps/mcp/src/server/tools/output-schemas.ts +++ b/apps/mcp/src/server/tools/output-schemas.ts @@ -7,11 +7,25 @@ import { sessionScopeSchema, } from "../../shared/types" +const documentStatusSchema = z + .string() + .describe( + "Document processing state. done means the content is processed; memories may still be pending, see dreamingStatus.", + ) + +const dreamingStatusSchema = z + .enum(["dreaming", "done"]) + .optional() + .describe( + "Memory extraction status, present when the API returns it. dreaming: pending, running, or retrying after a failure. done: settled, possibly with no memories. Neither done means memories are searchable yet.", + ) + const documentSummarySchema = z.object({ id: z.string(), title: z.string().nullable(), type: z.string(), - status: z.string(), + status: documentStatusSchema, + dreamingStatus: dreamingStatusSchema, createdAt: z.string(), updatedAt: z.string(), summary: z.string().nullable(), @@ -23,8 +37,8 @@ export const addMemoryOutputSchema = z.object({ namespace: z.string(), ...legacyNamespaceOutput, message: z.string(), - id: z.string().optional(), - status: z.string().optional(), + id: z.string().optional().describe("Accepted document ID, not a memory ID"), + status: documentStatusSchema.optional(), forgotten: z .array(z.object({ id: z.string(), memory: z.string() })) .optional(), @@ -49,7 +63,8 @@ export const getDocumentOutputSchema = z.object({ id: z.string(), title: z.string().nullable(), type: z.string(), - status: z.string(), + status: documentStatusSchema, + dreamingStatus: dreamingStatusSchema, createdAt: z.string(), updatedAt: z.string(), summary: z.string().nullable(),