@withpica/mcp-server
Version:
MCP Server for withPICA — enables AI assistants to interact with your withPICA music catalog
222 lines • 11.2 kB
JavaScript
// Copyright (c) 2025-2026 Withpica Ltd. All rights reserved.
import { formatSuccess, formatStructuredList, formatValidationError, } from "@withpica/mcp-utils";
export class MemoryTools {
pica;
constructor(pica) {
this.pica = pica;
}
getTools() {
return [
{
definition: {
name: "pica_memory_list",
// Rescoped admin -> read:catalog (ADR-289 Wave C2 review): matches
// the route's own gate (GET /api/admin/memory, read:catalog); admin
// over-scoped it AND was uncallable by the assistant grant
// (GRANT_EDITABLE_SCOPES excludes admin) — same fix as 174233bda.
scope: "read:catalog",
audiences: ["stdio", "http", "public"],
tier: "read",
description: "Use when the user asks: 'what have you remembered about me?', 'what notes do you have?', 'list my memories'. " +
"List memories for this workspace (personal + workspace-wide), most recently changed first, one page at a time. " +
"Use to recall stored context. When has_more is true, call again with offset=next_offset to read the rest. " +
"To match on a keyword rather than page through everything, use pica_memory_search.",
workflows: "infrastructure",
vernacular_kind: "creator-entry",
inputSchema: {
type: "object",
properties: {
limit: {
type: "number",
minimum: 1,
maximum: 100,
description: "Memories per page (default 50, max 100).",
},
offset: {
type: "number",
description: "How many memories to skip — pass the previous page's next_offset. Default 0.",
},
},
},
},
executor: this.listMemories.bind(this),
},
{
definition: {
name: "pica_memory_search",
// Rescoped admin -> read:catalog (ADR-289 Wave C2 review): matches
// the backing route's own gate (GET /api/admin/memory/search,
// requiredScope "read:catalog"). See pica_memory_list.
scope: "read:catalog",
audiences: ["stdio", "http", "public"],
tier: "read",
description: "Search memories by keyword. Use when you need to find specific stored context. " +
"To see every stored memory rather than keyword matches, use pica_memory_list.",
workflows: "infrastructure",
vernacular_kind: "specialist",
vernacular_reason: "Specialist context; not yet covered by the Creator Question Atlas — pending telemetry-driven addition per ADR-226 Phase 4 substrate writes.",
inputSchema: {
type: "object",
properties: {
query: {
type: "string",
description: "Search query to match against memory keys and content",
},
},
required: ["query"],
},
},
executor: this.searchMemories.bind(this),
},
{
definition: {
name: "pica_memory_save",
// Rescoped admin -> write:catalog (ADR-289 Wave C2 review): matches
// the backing route's own gate (POST /api/admin/memory,
// requiredScope "write:catalog") and the rescoped
// ADMIN_SEGMENT_WRITE_FLOOR["memory"]. See pica_memory_list.
scope: "write:catalog",
audiences: ["stdio", "http", "public"],
tier: "write",
description: "Use when the user asks: 'remember that I call X 'Y'', 'when I say jingles, I mean sync placements', 'save this vocabulary'. " +
"Save a memory for this workspace. Use to store important context, decisions, or preferences. " +
"To correct a memory that already exists, use pica_memory_update.",
workflows: "infrastructure",
vernacular_kind: "creator-entry",
inputSchema: {
type: "object",
properties: {
key: {
type: "string",
description: "Short identifier for this memory (e.g., 'sync_licensing_priority', 'publisher_x_requirements')",
},
content: {
type: "string",
description: "The memory content to store",
},
scope: {
type: "string",
enum: ["personal", "organisation"],
description: "Scope: 'personal' (only this user) or 'organisation' (shared with team). Default: personal.",
},
type: {
type: "string",
enum: [
"preference",
"business_rule",
"catalog_fact",
"decision",
"relationship",
],
description: "Type of memory. Default: preference.",
},
},
required: ["key", "content"],
},
},
executor: this.saveMemory.bind(this),
},
{
definition: {
name: "pica_memory_delete",
// Rescoped admin -> write:catalog (ADR-289 Wave C2 review): the
// route wants ["write:catalog", "destructive:*"]; tier:"destructive"
// layers destructive:* at dispatch (pica_audio_delete pattern).
// The assistant grant still can't call this (destructive:* is not
// grant-editable) — correct and by policy, see the commit body.
scope: "write:catalog",
audiences: ["stdio", "http", "public"],
tier: "destructive",
description: "Use when the user asks: 'forget what I told you about X', 'drop that memory', 'delete that note'. " +
"Delete a memory by ID. Use to remove outdated or incorrect stored context. " +
"If the content is merely out of date, use pica_memory_update instead — this removes the memory outright.",
workflows: "infrastructure",
vernacular_kind: "creator-entry",
inputSchema: {
type: "object",
properties: {
id: {
type: "string",
description: "Memory ID to delete",
},
},
required: ["id"],
},
},
executor: this.deleteMemory.bind(this),
},
{
definition: {
name: "pica_memory_update",
scope: "write:catalog",
audiences: ["stdio", "http", "public"],
tier: "write",
description: "Use when the user asks: 'actually, that's not right, it's...', " +
"'update that memory to say...', 'that note is out of date'. " +
"Update an existing memory's content by id. Use pica_memory_search or pica_memory_list " +
"first to find the id. Only content is updatable — to change scope or type, delete and " +
"re-save instead.",
workflows: "infrastructure",
vernacular_kind: "creator-entry",
inputSchema: {
type: "object",
properties: {
id: {
type: "string",
description: "Memory ID to update",
},
content: {
type: "string",
description: "The corrected memory content",
},
},
required: ["id", "content"],
},
},
executor: this.updateMemory.bind(this),
},
];
}
async listMemories(args) {
const offset = typeof args.offset === "number" ? args.offset : 0;
const page = await this.pica.memory.listPage({
limit: typeof args.limit === "number" ? args.limit : undefined,
offset,
});
return formatStructuredList(page.data, "memory", {
total: page.total,
has_more: page.hasMore,
...(page.hasMore && { next_offset: offset + page.data.length }),
});
}
async searchMemories(args) {
const memories = await this.pica.memory.search(args.query);
return formatStructuredList(memories, "memory", { query: args.query });
}
async saveMemory(args) {
const memory = await this.pica.memory.save({
key: args.key,
content: args.content,
scope: args.scope,
type: args.type,
});
return formatSuccess("Memory saved successfully", memory);
}
async deleteMemory(args) {
await this.pica.memory.delete(args.id);
return formatSuccess(`Memory ${args.id} deleted successfully`);
}
async updateMemory(args) {
if (!args.id?.trim()) {
return formatValidationError({ id: "id is required" });
}
if (!args.content?.trim()) {
return formatValidationError({ content: "content is required" });
}
const memory = await this.pica.memory.update(args.id, {
content: args.content.trim(),
});
return formatSuccess(`Memory ${args.id} updated successfully`, memory);
}
}
//# sourceMappingURL=memory.js.map