@withpica/mcp-server
Version:
MCP Server for withPICA — enables AI assistants to interact with your withPICA music catalog
183 lines • 8.02 kB
JavaScript
// 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