mirror of
https://github.com/supermemoryai/supermemory.git
synced 2026-10-26 06:04:08 +00:00
Merge d797bf249f into 23983bffae
This commit is contained in:
commit
1c7d26f703
13 changed files with 251 additions and 49 deletions
4
.github/workflows/ci.yml
vendored
4
.github/workflows/ci.yml
vendored
|
|
@ -96,5 +96,9 @@ jobs:
|
|||
- name: Run Memory Graph unit tests
|
||||
run: bun run --cwd packages/memory-graph test
|
||||
|
||||
- name: Run MCP unit tests
|
||||
working-directory: apps/mcp
|
||||
run: bun run test:unit
|
||||
|
||||
- name: Run Biome CI (format & lint on changed files)
|
||||
run: bunx biome ci --changed --since=origin/main --no-errors-on-unmatched
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ Your assistant chooses these tools automatically. Use this table when you need t
|
|||
| --- | --- | --- | --- |
|
||||
| `search_memory` | Semantic recall from one space | `query` (required), `containerTag` | Matching memories |
|
||||
| `get_profile` | Stable and recent profile context for one space | `containerTag` | Profile facts |
|
||||
| `add_memory` | Save information | `content` (required), `namespace` | Save confirmation |
|
||||
| `add_memory` | Save information | `content` (required), `namespace`, optional `metadata` | Save confirmation |
|
||||
| `forget_memory` | Forget outdated information | `content` (required), `namespace` | Forget confirmation |
|
||||
| `list_documents` | Browse stored source documents and their summaries | `page`, `limit`, `containerTag` | Document IDs, titles, types, status, dates, and summaries |
|
||||
| `get_document` | Read the available content of one document | `documentId` (required) | Document metadata, summary, and available content |
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@
|
|||
|
||||
API_URL=
|
||||
MCP_URL=
|
||||
# OAuth metadata is advertised from this, so set it to your local MCP URL + /mcp (defaults to prod).
|
||||
MCP_RESOURCE=
|
||||
|
||||
#(optional)
|
||||
POSTHOG_API_KEY=
|
||||
|
|
@ -39,8 +39,12 @@ Example client configuration:
|
|||
}
|
||||
```
|
||||
|
||||
The client discovers the OAuth authorization server through
|
||||
`/.well-known/oauth-protected-resource/mcp`.
|
||||
For the default resource, the client discovers the OAuth authorization server
|
||||
through `/.well-known/oauth-protected-resource/mcp`. Custom deployments derive
|
||||
this location from `MCP_RESOURCE` by inserting
|
||||
`/.well-known/oauth-protected-resource` before the resource path while
|
||||
preserving its query string. For example, `https://memory.example.com/gateway/mcp`
|
||||
uses `https://memory.example.com/.well-known/oauth-protected-resource/gateway/mcp`.
|
||||
|
||||
## Tools
|
||||
|
||||
|
|
@ -131,7 +135,7 @@ discovery and rejection tests still run.
|
|||
| Variable | Purpose | Default |
|
||||
| --- | --- | --- |
|
||||
| `API_URL` | Supermemory API and OAuth issuer | `https://api.supermemory.ai` |
|
||||
| `MCP_RESOURCE` | Expected OAuth audience | `https://mcp.supermemory.ai/mcp` |
|
||||
| `MCP_RESOURCE` | Expected OAuth audience and canonical resource URL used for metadata discovery | `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` |
|
||||
|
|
|
|||
|
|
@ -22,10 +22,12 @@ describeWithAuth("MCP - space scoping", () => {
|
|||
const plainTools = (await plain.client.listTools()).tools
|
||||
|
||||
expect(propsOf(plainTools, "add_memory")).toHaveProperty("namespace")
|
||||
expect(propsOf(plainTools, "add_memory")).toHaveProperty("metadata")
|
||||
expect(propsOf(plainTools, "search_memory")).toHaveProperty("namespace")
|
||||
expect(propsOf(plainTools, "get_profile")).toHaveProperty("namespace")
|
||||
|
||||
expect(propsOf(scopedTools, "add_memory")).toHaveProperty("namespace")
|
||||
expect(propsOf(scopedTools, "add_memory")).toHaveProperty("metadata")
|
||||
expect(propsOf(scopedTools, "search_memory")).toHaveProperty("namespace")
|
||||
expect(propsOf(scopedTools, "get_profile")).toHaveProperty("namespace")
|
||||
} finally {
|
||||
|
|
|
|||
|
|
@ -48,6 +48,11 @@ export interface DocumentSummary {
|
|||
|
||||
export type DocumentDetails = GetDocumentsResponse
|
||||
|
||||
export interface ForgetCandidate {
|
||||
id: string
|
||||
memory: string
|
||||
}
|
||||
|
||||
export interface DocumentsListResponse {
|
||||
documents: DocumentSummary[]
|
||||
pagination: ListResponsePagination
|
||||
|
|
@ -191,11 +196,12 @@ export class SupermemoryClient {
|
|||
|
||||
async createMemory(
|
||||
content: string,
|
||||
metadata?: Record<string, string | number | boolean | string[]>,
|
||||
): Promise<{ id: string; status: string; namespace: string }> {
|
||||
try {
|
||||
const result = await this.client.add(this.namespace, {
|
||||
content,
|
||||
metadata: { sm_source: MCP_SOURCE },
|
||||
metadata: { ...metadata, sm_source: MCP_SOURCE },
|
||||
})
|
||||
return {
|
||||
id: result.id,
|
||||
|
|
@ -207,39 +213,51 @@ export class SupermemoryClient {
|
|||
}
|
||||
}
|
||||
|
||||
// v5 has no exact-content forget: preview semantic matches, then forget the best one by ID.
|
||||
async forgetMemory(
|
||||
content: string,
|
||||
): Promise<{ success: boolean; message: string; namespace: string }> {
|
||||
// v5 has no exact-content forget: dry-run a semantic match so the caller can confirm it before forgetting by ID.
|
||||
async findMemoryToForget(content: string): Promise<ForgetCandidate | null> {
|
||||
try {
|
||||
const preview = await this.client.memories.forgetMatching(
|
||||
this.namespace,
|
||||
{ query: content, dryRun: true },
|
||||
)
|
||||
const match = preview.matches[0]
|
||||
if (!match) {
|
||||
return {
|
||||
success: false,
|
||||
message: "No matching memory found to forget.",
|
||||
namespace: this.namespace,
|
||||
}
|
||||
}
|
||||
return match ? { id: match.id, memory: match.memory } : null
|
||||
} catch (error) {
|
||||
this.handleOperationError("Forget memory request", error)
|
||||
}
|
||||
}
|
||||
|
||||
async getMemory(id: string): Promise<ForgetCandidate | null> {
|
||||
try {
|
||||
const memory = await this.client.memories.get(this.namespace, id)
|
||||
return memory.isForgotten
|
||||
? null
|
||||
: { id: memory.id, memory: memory.memory }
|
||||
} catch (error) {
|
||||
if (isNotFound(error)) return null
|
||||
this.handleOperationError("Get memory request", error)
|
||||
}
|
||||
}
|
||||
|
||||
async forgetMemoryById(
|
||||
candidate: ForgetCandidate,
|
||||
): Promise<{ success: boolean; message: string; namespace: string }> {
|
||||
try {
|
||||
const result = await this.client.memories.forget(this.namespace, {
|
||||
ids: [match.id],
|
||||
ids: [candidate.id],
|
||||
})
|
||||
if (result.count === 0) {
|
||||
const reason = result.errors[0]?.error ?? "unknown error"
|
||||
return {
|
||||
success: false,
|
||||
message: `Could not forget memory ${match.id}: ${reason}`,
|
||||
message: `Could not forget memory ${candidate.id}: ${reason}`,
|
||||
namespace: this.namespace,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: `Forgot memory (ID: ${match.id}): "${limitByChars(match.memory, 100)}"`,
|
||||
message: `Forgot memory (ID: ${candidate.id}): "${limitByChars(candidate.memory, 100)}"`,
|
||||
namespace: this.namespace,
|
||||
}
|
||||
} catch (error) {
|
||||
|
|
|
|||
|
|
@ -81,9 +81,20 @@ export function formatMemoryEntriesList(
|
|||
)
|
||||
|
||||
if (activeEntries.length === 0) {
|
||||
return pagination.currentPage > 1
|
||||
? `No active memories on page ${pagination.currentPage} (${pagination.totalPages} page${pagination.totalPages === 1 ? "" : "s"} total).`
|
||||
: "No active memories stored yet."
|
||||
if (pagination.currentPage === 1 && pagination.totalItems === 0) {
|
||||
return "No active memories stored yet."
|
||||
}
|
||||
|
||||
const parts = [
|
||||
`No active memories on page ${pagination.currentPage} (${pagination.totalPages} page${pagination.totalPages === 1 ? "" : "s"} total).`,
|
||||
]
|
||||
if (pagination.currentPage < pagination.totalPages) {
|
||||
parts.push(
|
||||
"",
|
||||
`More available - call list_memories with page: ${pagination.currentPage + 1}.`,
|
||||
)
|
||||
}
|
||||
return parts.join("\n")
|
||||
}
|
||||
|
||||
const blocks = activeEntries.map((entry) => {
|
||||
|
|
|
|||
|
|
@ -10,6 +10,10 @@ import {
|
|||
type AuthUser,
|
||||
} from "./auth"
|
||||
import { SupermemoryMCP } from "./legacy-protocol-state"
|
||||
import {
|
||||
PROTECTED_RESOURCE_METADATA_PATH,
|
||||
protectedResourceMetadataUrl,
|
||||
} from "./oauth-metadata"
|
||||
import { createSupermemoryServer } from "./server"
|
||||
import type { ActorContext, ServerEnv } from "./types"
|
||||
import { SpaceState, uploadStateName } from "./space-state"
|
||||
|
|
@ -20,8 +24,6 @@ const app = new Hono<{ Bindings: Bindings }>()
|
|||
|
||||
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 = [
|
||||
|
|
@ -64,6 +66,18 @@ app.get("/", (c) => {
|
|||
function resourceMetadata(c: Context<{ Bindings: Bindings }>) {
|
||||
const apiUrl = c.env.API_URL || DEFAULT_API_URL
|
||||
const mcpResource = c.env.MCP_RESOURCE || DEFAULT_MCP_RESOURCE
|
||||
const requestUrl = new URL(c.req.url)
|
||||
const canonicalMetadataUrl = new URL(
|
||||
protectedResourceMetadataUrl(mcpResource),
|
||||
)
|
||||
const isMcpRootFallback =
|
||||
requestUrl.pathname === PROTECTED_RESOURCE_METADATA_PATH &&
|
||||
requestUrl.search === ""
|
||||
const isCanonicalLocation =
|
||||
requestUrl.pathname === canonicalMetadataUrl.pathname &&
|
||||
requestUrl.search === canonicalMetadataUrl.search
|
||||
|
||||
if (!isMcpRootFallback && !isCanonicalLocation) return c.notFound()
|
||||
|
||||
return c.json({
|
||||
resource: mcpResource,
|
||||
|
|
@ -74,8 +88,8 @@ function resourceMetadata(c: Context<{ Bindings: Bindings }>) {
|
|||
})
|
||||
}
|
||||
|
||||
app.get("/.well-known/oauth-protected-resource", resourceMetadata)
|
||||
app.get(PROTECTED_RESOURCE_METADATA_PATH, resourceMetadata)
|
||||
app.get(`${PROTECTED_RESOURCE_METADATA_PATH}/*`, resourceMetadata)
|
||||
|
||||
app.get("/.well-known/openai-apps-challenge", (c) => {
|
||||
return c.text(c.env.OPENAI_APPS_CHALLENGE || "")
|
||||
|
|
@ -197,11 +211,7 @@ async function handleMcpRequest(
|
|||
const apiUrl = c.env.API_URL || DEFAULT_API_URL
|
||||
const mcpResource = c.env.MCP_RESOURCE || DEFAULT_MCP_RESOURCE
|
||||
|
||||
const reqHost = c.req.header("x-forwarded-host") || c.req.header("host") || ""
|
||||
const reqProto = c.req.header("x-forwarded-proto") || "https"
|
||||
const resourceMetadataUrl = reqHost
|
||||
? `${reqProto}://${reqHost}${PROTECTED_RESOURCE_METADATA_PATH}`
|
||||
: PROTECTED_RESOURCE_METADATA_PATH
|
||||
const resourceMetadataUrl = protectedResourceMetadataUrl(mcpResource)
|
||||
const mcpOrigin = c.env.MCP_PUBLIC_ORIGIN || new URL(mcpResource).origin
|
||||
|
||||
if (!token) return unauthorizedResponse(resourceMetadataUrl)
|
||||
|
|
|
|||
10
apps/mcp/src/server/oauth-metadata.ts
Normal file
10
apps/mcp/src/server/oauth-metadata.ts
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
export const PROTECTED_RESOURCE_METADATA_PATH =
|
||||
"/.well-known/oauth-protected-resource"
|
||||
|
||||
// Discovery must use the configured resource, never request-controlled forwarding headers.
|
||||
export function protectedResourceMetadataUrl(resource: string): string {
|
||||
const resourceUrl = new URL(resource)
|
||||
const resourcePath = resourceUrl.pathname === "/" ? "" : resourceUrl.pathname
|
||||
resourceUrl.pathname = `${PROTECTED_RESOURCE_METADATA_PATH}${resourcePath}`
|
||||
return resourceUrl.toString()
|
||||
}
|
||||
|
|
@ -10,6 +10,14 @@ import { forgetMemory } from "./forget-memory"
|
|||
import { addMemoryOutputSchema, type AddMemoryOutput } from "./output-schemas"
|
||||
import { textContent, type ToolDeps } from "./types"
|
||||
|
||||
export const memoryMetadataSchema = z
|
||||
.record(
|
||||
z.string(),
|
||||
z.union([z.string(), z.number(), z.boolean(), z.array(z.string())]),
|
||||
)
|
||||
.optional()
|
||||
.describe("Optional metadata to attach to the saved memory")
|
||||
|
||||
export function register(deps: ToolDeps) {
|
||||
const inputSchema = z.object({
|
||||
content: z
|
||||
|
|
@ -25,6 +33,7 @@ export function register(deps: ToolDeps) {
|
|||
),
|
||||
namespace: optionalNamespaceSchema,
|
||||
...legacyNamespaceInput,
|
||||
metadata: memoryMetadataSchema,
|
||||
})
|
||||
|
||||
deps.server.registerTool(
|
||||
|
|
@ -36,13 +45,13 @@ export function register(deps: ToolDeps) {
|
|||
outputSchema: addMemoryOutputSchema,
|
||||
annotations: MEMORY_TOOL_ANNOTATIONS,
|
||||
},
|
||||
async (args) => {
|
||||
if (args.action === "forget") return forgetMemory(deps, args)
|
||||
async (args, context) => {
|
||||
if (args.action === "forget") return forgetMemory(deps, args, context)
|
||||
try {
|
||||
const namespace = await deps.resolveNamespace(namespaceArg(args))
|
||||
const client = deps.getClient(namespace)
|
||||
|
||||
const result = await client.createMemory(args.content)
|
||||
const result = await client.createMemory(args.content, args.metadata)
|
||||
const message = `Memory saved (ID: ${result.id}, space: ${result.namespace})`
|
||||
const structuredContent: AddMemoryOutput = withLegacyNamespace({
|
||||
action: "save",
|
||||
|
|
|
|||
|
|
@ -1,4 +1,13 @@
|
|||
import {
|
||||
CLIENT_CAPABILITIES_META_KEY,
|
||||
type CallToolResult,
|
||||
type InputRequiredResult,
|
||||
inputRequired,
|
||||
inputResponse,
|
||||
type ServerContext,
|
||||
} from "@modelcontextprotocol/server"
|
||||
import { z } from "zod"
|
||||
import type { ForgetCandidate } from "../client"
|
||||
import { optionalNamespaceSchema } from "../namespace"
|
||||
import { MEMORY_TOOL_ANNOTATIONS } from "./annotations"
|
||||
import {
|
||||
|
|
@ -9,22 +18,127 @@ import {
|
|||
import { forgetMemoryOutputSchema } from "./output-schemas"
|
||||
import { textContent, type ToolDeps } from "./types"
|
||||
|
||||
const CONFIRM_KEY = "confirm_forget"
|
||||
|
||||
export interface ForgetMemoryArgs {
|
||||
content?: string
|
||||
id?: string
|
||||
confirm?: boolean
|
||||
namespace?: string
|
||||
containerTag?: string
|
||||
}
|
||||
|
||||
// Stateless legacy requests cannot receive elicitation, so they use explicit two-call confirmation.
|
||||
function canElicit(context: ServerContext | undefined): boolean {
|
||||
const envelope = context?.mcpReq?.envelope
|
||||
if (!envelope) return false
|
||||
const capabilities = Reflect.get(envelope, CLIENT_CAPABILITIES_META_KEY)
|
||||
if (!capabilities || typeof capabilities !== "object") return false
|
||||
const elicitation = Reflect.get(capabilities, "elicitation")
|
||||
return Boolean(
|
||||
elicitation &&
|
||||
typeof elicitation === "object" &&
|
||||
(Reflect.get(elicitation, "form") !== undefined ||
|
||||
Reflect.get(elicitation, "url") === undefined),
|
||||
)
|
||||
}
|
||||
|
||||
function askToConfirm(
|
||||
candidate: ForgetCandidate,
|
||||
namespace: string,
|
||||
): InputRequiredResult {
|
||||
return inputRequired({
|
||||
inputRequests: {
|
||||
[CONFIRM_KEY]: inputRequired.elicit({
|
||||
message: `Forget this memory from "${namespace}"?\n\n"${candidate.memory}"`,
|
||||
requestedSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
confirm: {
|
||||
type: "boolean",
|
||||
title: "Yes, forget this memory",
|
||||
},
|
||||
},
|
||||
required: ["confirm"],
|
||||
},
|
||||
}),
|
||||
},
|
||||
requestState: candidate.id,
|
||||
})
|
||||
}
|
||||
|
||||
function result(
|
||||
namespace: string,
|
||||
status: "forgotten" | "confirmation_required" | "cancelled" | "not_found",
|
||||
message: string,
|
||||
options: { success?: boolean; candidate?: ForgetCandidate } = {},
|
||||
): CallToolResult {
|
||||
return {
|
||||
content: [textContent(message)],
|
||||
structuredContent: withLegacyNamespace({
|
||||
action: "forget" as const,
|
||||
success: options.success ?? false,
|
||||
namespace,
|
||||
message,
|
||||
status,
|
||||
id: options.candidate?.id,
|
||||
memory: options.candidate?.memory,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
export async function forgetMemory(
|
||||
deps: ToolDeps,
|
||||
args: { content: string; namespace?: string; containerTag?: string },
|
||||
) {
|
||||
args: ForgetMemoryArgs,
|
||||
context?: ServerContext,
|
||||
): Promise<CallToolResult | InputRequiredResult> {
|
||||
try {
|
||||
const namespace = await deps.resolveNamespace(namespaceArg(args))
|
||||
const result = await deps.getClient(namespace).forgetMemory(args.content)
|
||||
return {
|
||||
content: [textContent(result.message)],
|
||||
structuredContent: withLegacyNamespace({
|
||||
action: "forget" as const,
|
||||
success: result.success,
|
||||
namespace: result.namespace,
|
||||
message: result.message,
|
||||
}),
|
||||
if (!args.id && !args.content) {
|
||||
throw new Error("Provide the memory id or content to forget.")
|
||||
}
|
||||
const namespace = await deps.resolveNamespace(namespaceArg(args))
|
||||
const client = deps.getClient(namespace)
|
||||
const candidate = args.id
|
||||
? await client.getMemory(args.id)
|
||||
: await client.findMemoryToForget(args.content ?? "")
|
||||
if (!candidate) {
|
||||
return result(
|
||||
namespace,
|
||||
"not_found",
|
||||
"No matching memory found to forget.",
|
||||
)
|
||||
}
|
||||
|
||||
if (canElicit(context)) {
|
||||
const answer = inputResponse(context?.mcpReq.inputResponses, CONFIRM_KEY)
|
||||
const askedId = context?.mcpReq.requestState?.()
|
||||
if (answer.kind !== "elicit" || askedId !== candidate.id) {
|
||||
return askToConfirm(candidate, namespace)
|
||||
}
|
||||
if (answer.action !== "accept" || answer.content?.confirm !== true) {
|
||||
return result(
|
||||
namespace,
|
||||
"cancelled",
|
||||
"The user chose to keep this memory. Nothing was forgotten.",
|
||||
{ candidate },
|
||||
)
|
||||
}
|
||||
} else if (!(args.confirm === true && args.id === candidate.id)) {
|
||||
return result(
|
||||
namespace,
|
||||
"confirmation_required",
|
||||
`Nothing forgotten yet. Show the user this memory and ask whether to forget it:\n"${candidate.memory}" (ID: ${candidate.id})\nOnly if they say yes, call forget_memory again with id "${candidate.id}" and confirm: true.`,
|
||||
{ candidate },
|
||||
)
|
||||
}
|
||||
|
||||
const forgotten = await client.forgetMemoryById(candidate)
|
||||
return result(
|
||||
forgotten.namespace,
|
||||
forgotten.success ? "forgotten" : "not_found",
|
||||
forgotten.message,
|
||||
{ success: forgotten.success, candidate },
|
||||
)
|
||||
} catch (error) {
|
||||
return deps.errorResult(error)
|
||||
}
|
||||
|
|
@ -35,18 +149,33 @@ export function register(deps: ToolDeps) {
|
|||
"forget_memory",
|
||||
{
|
||||
description:
|
||||
"Forget a memory matching the supplied content in the user's ACTIVE space. Use when information is outdated or the user asks to remove it. The target space is the one selected via select-space; pass namespace only to override it.",
|
||||
"Forget one memory in the user's ACTIVE space. Forgetting always needs the user's confirmation: clients that support elicitation show the user a confirmation prompt; otherwise the first call only returns the matching memory, and you must show it to the user and call again with its id and confirm: true once they agree. Prefer passing an id from search_memory or list_memories; content is matched semantically. Pass namespace only to override the active space.",
|
||||
inputSchema: z.object({
|
||||
content: z
|
||||
.string()
|
||||
.max(200000, "Content exceeds maximum length")
|
||||
.describe("The memory content to forget"),
|
||||
.optional()
|
||||
.describe(
|
||||
"Text describing the memory to forget, matched semantically",
|
||||
),
|
||||
id: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe(
|
||||
"Exact memory ID to forget, from search_memory or list_memories",
|
||||
),
|
||||
confirm: z
|
||||
.boolean()
|
||||
.optional()
|
||||
.describe(
|
||||
"Set to true only after the user explicitly agreed to forget the memory with this id",
|
||||
),
|
||||
namespace: optionalNamespaceSchema,
|
||||
...legacyNamespaceInput,
|
||||
}),
|
||||
outputSchema: forgetMemoryOutputSchema,
|
||||
annotations: MEMORY_TOOL_ANNOTATIONS,
|
||||
},
|
||||
async (args) => forgetMemory(deps, args),
|
||||
async (args, context) => forgetMemory(deps, args, context),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ export const addMemoryOutputSchema = z.object({
|
|||
message: z.string(),
|
||||
id: z.string().optional(),
|
||||
status: z.string().optional(),
|
||||
memory: z.string().optional(),
|
||||
})
|
||||
|
||||
export type AddMemoryOutput = z.infer<typeof addMemoryOutputSchema>
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { saveSuccessViewSchema, type ViewMessage } from "../../shared/types"
|
|||
import { appResultMeta, appToolMeta } from "../app-metadata"
|
||||
import { namespaceSchema } from "../namespace"
|
||||
import { legacyNamespaceInput, namespaceArg, withLegacyView } from "./compat"
|
||||
import { memoryMetadataSchema } from "./add-memory"
|
||||
import { ADDITIVE_MEMORY_TOOL_ANNOTATIONS } from "./annotations"
|
||||
import { textContent, type ToolDeps } from "./types"
|
||||
|
||||
|
|
@ -13,6 +14,7 @@ export function register(deps: ToolDeps) {
|
|||
description: "Save content to memory",
|
||||
inputSchema: z.object({
|
||||
content: z.string().min(1),
|
||||
metadata: memoryMetadataSchema,
|
||||
namespace: namespaceSchema.optional(),
|
||||
...legacyNamespaceInput,
|
||||
viewId: z.string().uuid().optional(),
|
||||
|
|
@ -28,7 +30,7 @@ export function register(deps: ToolDeps) {
|
|||
return deps.errorResult(new Error("namespace is required"))
|
||||
const viewId = args.viewId ?? crypto.randomUUID()
|
||||
const client = deps.getClient(namespace)
|
||||
const result = await client.createMemory(args.content)
|
||||
const result = await client.createMemory(args.content, args.metadata)
|
||||
const sc: ViewMessage = withLegacyView({
|
||||
view: "save-success",
|
||||
viewId,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue