UNPKG

@withpica/mcp-server

Version:

MCP Server for withPICA — enables AI assistants to interact with your withPICA music catalog

222 lines • 11.2 kB
// 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