UNPKG

@withpica/mcp-server

Version:

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

183 lines • 8.02 kB
// Copyright (c) 2025-2026 Withpica Ltd. All rights reserved. /** * ADR-230 — derive the dominant tier for a row in * `assistant_interactions`. The substrate stores `tools_used: text[]` * but no `tier` column of its own (Stage 1 decision: Option A — JOIN * at read time rather than denormalize). * * If a row's tools_used spans multiple tiers (e.g. a workflow that * runs `pica_works_query` then `pica_works_create` then * `pica_works_delete`), surface the most-impactful tier — agents * inspecting their own activity want to see the highest-stakes call, * not the first one. Order: destructive > write > draft > read. * * The tier resolver is injected so this module stays decoupled from * the registry it reads. ToolRegistry passes * `(name) => this.tools.get(name)?.definition.tier` — the declared * `tier` on each tool definition is the source of truth (lint Rule 13 * makes it required for every customer MCP tool). */ const TIER_RANK = { read: 0, draft: 1, write: 2, destructive: 3, }; function dominantTier(tools, getTier) { if (!Array.isArray(tools) || tools.length === 0) return "read"; let highest = "read"; for (const t of tools) { const tier = getTier(t) ?? "read"; if (TIER_RANK[tier] > TIER_RANK[highest]) { highest = tier; } } return highest; } export class MyRecentQuestionsTools { pica; getTier; constructor(pica, getTier) { this.pica = pica; this.getTier = getTier; } getTools() { return [ { definition: { name: "pica_my_recent_questions", scope: "read:analytics", audiences: ["stdio", "http"], tier: "read", description: "Use when the user asks: 'what did I ask the AI?', 'what have I been doing?', 'show me my recent activity', 'what AI tasks did I confirm?'. " + "Returns the user's recent questions, what tools the AI ran, and whether each succeeded. " + "Renders the originating client per row by joining mcp_sessions, so 'Claude Desktop did X / ChatGPT did Y' is visible at a glance. " + "Each row carries `tier` (read | draft | write | destructive — the most-impactful tier across the row's tools_used) so agents can scan stakes at a glance. " + "To pick up an open loop from these rows, ask your assistant to continue where you left off — surfaced via the client's prompts feature, not a callable tool.", workflows: "infrastructure", vernacular_kind: "creator-entry", inputSchema: { type: "object", properties: { session_id: { type: "string", description: "Optional. Filter to a single MCP session — useful for replaying one conversation.", }, since: { type: "string", description: "Optional. ISO 8601 timestamp lower bound (e.g. '2026-05-01T00:00:00Z').", }, success: { type: "boolean", description: "Optional. Pass `true` to see only successful interactions, `false` to see only failures.", }, intent: { type: "string", description: "Optional. Filter to a specific intent (e.g. 'pica_works_create').", }, limit: { type: "number", minimum: 1, maximum: 100, description: "Optional. Default 25, max 100.", }, }, }, }, executor: this.listMine.bind(this), }, ]; } async listMine(args) { const limitArg = typeof args.limit === "number" ? args.limit : undefined; const limit = limitArg !== undefined ? Math.min(Math.max(limitArg, 1), 100) : undefined; const rawRows = await this.pica.assistantInteractions.listRecent({ sessionId: typeof args.session_id === "string" ? args.session_id : undefined, since: typeof args.since === "string" ? args.since : undefined, success: typeof args.success === "boolean" ? args.success : undefined, intent: typeof args.intent === "string" ? args.intent : undefined, limit, }); // ADR-230 — derive tier per row at read time from tools_used // (assistant_interactions has no tier column; Stage 1 picked // Option A — registry-derived lookup over column denormalisation). const rows = Array.isArray(rawRows) ? rawRows.map((r) => ({ ...r, tier: dominantTier((Array.isArray(r.tools_used) ? r.tools_used : []), this.getTier), })) : rawRows; if (!Array.isArray(rows) || rows.length === 0) { return { content: [ { type: "text", text: "No recent activity for your account. Once you start asking PICA to do things, this is where you'll see them.", }, ], structuredContent: { kind: "pica.card.my_recent_questions", version: 1, data: { count: 0, interactions: [] }, }, }; } const lines = rows.slice(0, 10).map((r) => { const tag = r.success ? r.required_confirmation && r.was_confirmed !== true ? "⏳ awaiting" : "✓" : "✗"; const client = r.client_name ? `${r.client_name}${r.client_version ? ` v${r.client_version}` : ""}` : "unknown client"; const summary = r.result_summary ?? r.prompt_excerpt ?? r.intent; const tools = Array.isArray(r.tools_used) && r.tools_used.length > 0 ? ` — \`${r.tools_used.slice(0, 3).join(", ")}\`` : ""; const when = formatWhen(r.created_at); return `[${tag}] ${client} ${when}: ${truncate(summary, 140)}${tools}`; }); const more = rows.length > 10 ? `\n\n_+${rows.length - 10} more — pass a higher limit to see them._` : ""; return { content: [ { type: "text", text: `${rows.length} recent interaction${rows.length === 1 ? "" : "s"}:\n\n` + `${lines.join("\n")}${more}`, }, ], structuredContent: { kind: "pica.card.my_recent_questions", version: 1, data: { count: rows.length, interactions: rows, }, }, }; } } function truncate(text, max) { if (text.length <= max) return text; return text.slice(0, max - 1) + "…"; } function formatWhen(iso) { const then = new Date(iso).getTime(); const diffMs = Date.now() - then; const minutes = Math.floor(diffMs / 60000); if (minutes < 1) return "just now"; if (minutes < 60) return `${minutes}m ago`; const hours = Math.floor(minutes / 60); if (hours < 24) return `${hours}h ago`; const days = Math.floor(hours / 24); return `${days}d ago`; } //# sourceMappingURL=my-recent-questions.js.map