Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions docs/use/memory.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,9 @@ Use these through **`execute`**:
- **`meta_memory_get`** — load one stored memory by id
- **`meta_memory_search`** — browse/search stored memories directly

Memory records can also include optional **`source_uris`** — opaque canonical
document URLs such as GitHub files, R2 object URLs, or Notion pages.

## Categories

Memory categories are freeform strings. Kody does not force a closed list.
Expand Down
2 changes: 2 additions & 0 deletions packages/worker/migrations/0018-mcp-memory-source-uris.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
ALTER TABLE mcp_memories
ADD COLUMN source_uris_json TEXT NOT NULL DEFAULT '[]';
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ function formatMemoryRecord(
summary: memory.summary,
details: memory.details,
tags: memory.tags,
source_uris: memory.sourceUris,
dedupe_key: memory.dedupeKey,
created_at: memory.createdAt,
updated_at: memory.updatedAt,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export const metaMemoryGetCapability = defineDomainCapability(
summary: memory.summary,
details: memory.details,
tags: memory.tags,
source_uris: memory.sourceUris,
dedupe_key: memory.dedupeKey,
created_at: memory.createdAt,
updated_at: memory.updatedAt,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ export const metaMemorySearchCapability = defineDomainCapability(
summary: match.summary,
details: match.details,
tags: match.tags,
source_uris: match.sourceUris,
dedupe_key: match.dedupeKey,
created_at: match.createdAt,
updated_at: match.updatedAt,
Expand Down
14 changes: 14 additions & 0 deletions packages/worker/src/mcp/capabilities/meta/meta-memory-shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,26 @@ export const memoryCategoryField = z
'Optional freeform category label for the memory. Suggested examples: preference, identifier, relationship, workflow, project, profile.',
)

const maxSourceUriLength = 2_048
const maxSourceUriCount = 12

export const memoryTagInputSchema = z.string().min(1).max(80)
export const memorySourceUriSchema = z.string().url().max(maxSourceUriLength)

export const memoryTagsField = z
.array(memoryTagInputSchema)
.max(12)
.optional()
.describe('Optional short tags for retrieval and filtering.')

export const memorySourceUrisField = z
.array(memorySourceUriSchema)
.max(maxSourceUriCount)
.optional()
.describe(
'Optional canonical source document URLs for the memory. Treat these as opaque references.',
)

export const memoryBaseInputSchema = {
subject: z
.string()
Expand All @@ -37,6 +49,7 @@ export const memoryBaseInputSchema = {
.describe('Optional supporting details for the durable memory record.'),
category: memoryCategoryField,
tags: memoryTagsField,
source_uris: memorySourceUrisField,
dedupe_key: z
.string()
.min(1)
Expand Down Expand Up @@ -81,6 +94,7 @@ export const memoryRecordSchema = z.object({
summary: z.string(),
details: z.string(),
tags: z.array(z.string()),
source_uris: z.array(memorySourceUriSchema).optional(),
dedupe_key: z.string().nullable(),
created_at: z.string(),
updated_at: z.string(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { type CapabilityContext } from '#mcp/capabilities/types.ts'
import { upsertMemory } from '#mcp/memory/service.ts'
import {
memoryRecordSchema,
memorySourceUrisField,
memoryTagInputSchema,
} from '#mcp/capabilities/meta/meta-memory-shared.ts'
import { requireMcpUser } from './require-user.ts'
Expand Down Expand Up @@ -44,6 +45,7 @@ const inputSchema = z.object({
.max(12)
.optional()
.describe('Optional tags for retrieval and filtering.'),
source_uris: memorySourceUrisField,
dedupe_key: z
.string()
.max(160)
Expand Down Expand Up @@ -110,6 +112,7 @@ export const metaMemoryUpsertCapability = defineDomainCapability(
summary: args.summary,
details: args.details ?? '',
tags: args.tags ?? [],
sourceUris: args.source_uris ?? [],
dedupeKey: args.dedupe_key ?? null,
status: args.status ?? 'active',
verificationReference: args.verification_reference ?? null,
Expand All @@ -124,6 +127,7 @@ export const metaMemoryUpsertCapability = defineDomainCapability(
summary: result.memory.summary,
details: result.memory.details,
tags: result.memory.tags,
source_uris: result.memory.sourceUris,
dedupe_key: result.memory.dedupeKey,
created_at: result.memory.createdAt,
updated_at: result.memory.updatedAt,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { type CapabilityContext } from '#mcp/capabilities/types.ts'
import { verifyMemoryCandidate } from '#mcp/memory/service.ts'
import {
memoryRecordSchema,
memorySourceUriSchema,
memoryVerifyInputSchema,
requireMcpUser,
} from './meta-memory-shared.ts'
Expand All @@ -21,6 +22,7 @@ const relatedMemorySchema = memoryRecordSchema
subject: true,
summary: true,
tags: true,
source_uris: true,
dedupe_key: true,
})
.extend({
Expand All @@ -34,6 +36,7 @@ const outputSchema = z.object({
details: z.string(),
category: z.string().nullable(),
tags: z.array(z.string()),
source_uris: z.array(memorySourceUriSchema).optional(),
dedupe_key: z.string().nullable(),
}),
related_memories: z.array(relatedMemorySchema),
Expand Down Expand Up @@ -65,6 +68,7 @@ export const metaMemoryVerifyCapability = defineDomainCapability(
summary: args.summary,
details: args.details ?? null,
tags: args.tags ?? null,
sourceUris: args.source_uris ?? null,
dedupeKey: args.dedupe_key ?? null,
},
limit: args.limit,
Expand All @@ -91,6 +95,7 @@ function formatMemoryMatch(
subject: string
summary: string
tags: Array<string>
sourceUris: Array<string>
dedupeKey: string | null
},
score: number,
Expand All @@ -102,6 +107,7 @@ function formatMemoryMatch(
subject: match.subject,
summary: match.summary,
tags: match.tags,
source_uris: match.sourceUris,
dedupe_key: match.dedupeKey,
score,
}
Expand Down
60 changes: 59 additions & 1 deletion packages/worker/src/mcp/mcp-server.mcp-e2e.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,16 +59,74 @@ test('authenticated mcp client can list tools, execute codemode, and search memo
summary: 'Always use npm commands in this repository.',
category: 'preference',
tags: ['package-manager', 'repo-workflow'],
source_uris: [
'https://docs.npmjs.com/cli/v11/commands/npm-install',
'https://github.com/kentcdodds/kody/blob/main/AGENTS.md',
],
verified_by_agent: true,
verification_reference: 'verify-search-fallback-1',
})
}`,
},
})
const upsertStructured = (upsertResult as CallToolResult).structuredContent as
| { result?: { memory?: { id?: string } } }
| { result?: { memory?: { id?: string; source_uris?: Array<string> } } }
| undefined
expect(typeof upsertStructured?.result?.memory?.id).toBe('string')
expect(upsertStructured?.result?.memory?.source_uris).toEqual([
'https://docs.npmjs.com/cli/v11/commands/npm-install',
'https://github.com/kentcdodds/kody/blob/main/AGENTS.md',
])

const memoryId = upsertStructured?.result?.memory?.id
if (!memoryId) {
throw new Error('missing memoryId')
}

const getResult = await mcpClient.client.callTool({
name: 'execute',
arguments: {
code: `async () => {
return await codemode.meta_memory_get({
memory_id: ${JSON.stringify(memoryId)},
})
}`,
},
})
const getStructured = (getResult as CallToolResult).structuredContent as
| { result?: { source_uris?: Array<string> } }
| undefined
expect(getStructured?.result?.source_uris).toEqual([
'https://docs.npmjs.com/cli/v11/commands/npm-install',
'https://github.com/kentcdodds/kody/blob/main/AGENTS.md',
])

const memoryCapabilitySearchResult = await mcpClient.client.callTool({
name: 'execute',
arguments: {
code: `async () => {
return await codemode.meta_memory_search({
query: 'npm over pnpm',
limit: 3,
})
}`,
},
})
const memoryCapabilitySearchStructured = (
memoryCapabilitySearchResult as CallToolResult
).structuredContent as
| {
result?: {
matches?: Array<{ source_uris?: Array<string> }>
}
}
| undefined
expect(
memoryCapabilitySearchStructured?.result?.matches?.[0]?.source_uris,
).toEqual([
'https://docs.npmjs.com/cli/v11/commands/npm-install',
'https://github.com/kentcdodds/kody/blob/main/AGENTS.md',
])

const query = 'npm over pnpm'
const memorySearchResult = await mcpClient.client.callTool({
Expand Down
5 changes: 5 additions & 0 deletions packages/worker/src/mcp/memory/memory-search.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ export async function searchMemories(input: {
summary: row.summary,
details: row.details,
tags: parseTags(row.tags_json),
sourceUris: parseJsonStringArray(row.source_uris_json),
dedupeKey: row.dedupe_key,
createdAt: row.created_at,
updatedAt: row.updated_at,
Expand All @@ -122,6 +123,10 @@ export async function searchMemories(input: {
}

function parseTags(raw: string): Array<string> {
return parseJsonStringArray(raw)
}

function parseJsonStringArray(raw: string): Array<string> {
try {
const parsed = JSON.parse(raw) as unknown
if (!Array.isArray(parsed)) return []
Expand Down
15 changes: 10 additions & 5 deletions packages/worker/src/mcp/memory/repo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ function mapMemoryRow(row: Record<string, unknown>): McpMemoryRow {
summary: String(row['summary']),
details: String(row['details'] ?? ''),
tags_json: String(row['tags_json'] ?? '[]'),
source_uris_json: String(row['source_uris_json'] ?? '[]'),
dedupe_key: row['dedupe_key'] == null ? null : String(row['dedupe_key']),
created_at: String(row['created_at']),
updated_at: String(row['updated_at']),
Expand Down Expand Up @@ -47,8 +48,8 @@ export async function insertMemory(
.prepare(
`INSERT INTO mcp_memories (
id, user_id, category, status, subject, summary, details, tags_json,
dedupe_key, created_at, updated_at, last_accessed_at, deleted_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
source_uris_json, dedupe_key, created_at, updated_at, last_accessed_at, deleted_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
)
.bind(
row.id,
Expand All @@ -59,6 +60,7 @@ export async function insertMemory(
row.summary,
row.details,
row.tags_json,
row.source_uris_json,
row.dedupe_key,
row.created_at ?? now,
row.updated_at ?? now,
Expand All @@ -76,7 +78,7 @@ export async function getMemoryById(
const row = await db
.prepare(
`SELECT id, user_id, category, status, subject, summary, details, tags_json,
dedupe_key, created_at, updated_at, last_accessed_at, deleted_at
source_uris_json, dedupe_key, created_at, updated_at, last_accessed_at, deleted_at
FROM mcp_memories
WHERE user_id = ? AND id = ?
LIMIT 1`,
Expand All @@ -97,6 +99,7 @@ export async function updateMemory(
summary: string
details: string
tags_json: string
source_uris_json: string
dedupe_key: string | null
last_accessed_at: string | null
deleted_at: string | null
Expand All @@ -111,6 +114,7 @@ export async function updateMemory(
summary = ?,
details = ?,
tags_json = ?,
source_uris_json = ?,
dedupe_key = ?,
last_accessed_at = ?,
deleted_at = ?,
Expand All @@ -124,6 +128,7 @@ export async function updateMemory(
fields.summary,
fields.details,
fields.tags_json,
fields.source_uris_json,
fields.dedupe_key,
fields.last_accessed_at,
fields.deleted_at,
Expand Down Expand Up @@ -164,7 +169,7 @@ export async function listMemoriesByUserId(
const { results } = await db
.prepare(
`SELECT id, user_id, category, status, subject, summary, details, tags_json,
dedupe_key, created_at, updated_at, last_accessed_at, deleted_at
source_uris_json, dedupe_key, created_at, updated_at, last_accessed_at, deleted_at
FROM mcp_memories
WHERE user_id = ?
${statusClause}
Expand All @@ -182,7 +187,7 @@ export async function listAllMemories(input: {
const { results } = await input.db
.prepare(
`SELECT id, user_id, category, status, subject, summary, details, tags_json,
dedupe_key, created_at, updated_at, last_accessed_at, deleted_at
source_uris_json, dedupe_key, created_at, updated_at, last_accessed_at, deleted_at
FROM mcp_memories
ORDER BY updated_at DESC`,
)
Expand Down
Loading
Loading