UNPKG

@withpica/mcp-server

Version:

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

301 lines • 14 kB
// Copyright (c) 2025-2026 Withpica Ltd. All rights reserved. import { CREATE_AGENT_IDENTITY_WRITE_PROPERTIES, CREATE_AGENT_IDENTITY_REQUIRED, ISSUE_AGENT_GRANT_WRITE_PROPERTIES, ISSUE_AGENT_GRANT_REQUIRED, LIST_MY_AGENT_GRANTS_WRITE_PROPERTIES, LIST_MY_AGENT_GRANTS_REQUIRED, REVOKE_AGENT_GRANT_WRITE_PROPERTIES, REVOKE_AGENT_GRANT_REQUIRED, GET_AGENT_ACTIVITY_WRITE_PROPERTIES, GET_AGENT_ACTIVITY_REQUIRED, } from "@withpica/mcp-utils"; export class AgentIdentityTools { pica; constructor(pica) { this.pica = pica; } getTools() { return [ { definition: { name: "pica_create_agent_identity", scope: "write:agent_identity", audiences: ["stdio", "http", "public"], tier: "write", description: "Create a named agent identity (e.g. 'Claude on laptop') that grants can be issued against. Session-auth only — refused if called with a pica_grant_ token. Subject is the authenticated user; caller-supplied user_id is dropped.", 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: CREATE_AGENT_IDENTITY_WRITE_PROPERTIES, required: CREATE_AGENT_IDENTITY_REQUIRED, }, }, executor: this.createIdentity.bind(this), }, { definition: { name: "pica_issue_agent_grant", scope: "write:agent_identity", audiences: ["stdio", "http", "public"], tier: "write", description: "Issue a capability grant against an identity you own. Returns a one-time grant_token (format pica_grant_<random>) — save it immediately; it is never shown again. Default expiry 90 days, max 365. Session-auth only. " + "Scope strings are coarse: `write:catalog` covers works/recordings/credits/splits/enrichment together — do NOT send domain-specific scopes like `write:works`, `write:credits`, or `write:enrichment`; they will be rejected by VALID_SCOPES. Full list available via pica_tool_details('pica_issue_agent_grant').", 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: ISSUE_AGENT_GRANT_WRITE_PROPERTIES, required: ISSUE_AGENT_GRANT_REQUIRED, }, }, executor: this.issueGrant.bind(this), }, { definition: { name: "pica_list_my_agent_grants", scope: "write:agent_identity", audiences: ["stdio", "http", "public"], tier: "read", description: "List your active agent grants. Returns non-secret fields only — tokens are never re-exposed after issue. Session-auth only.", 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: LIST_MY_AGENT_GRANTS_WRITE_PROPERTIES, required: LIST_MY_AGENT_GRANTS_REQUIRED, }, }, executor: this.listMyGrants.bind(this), }, { definition: { name: "pica_revoke_agent_grant", scope: "write:agent_identity", audiences: ["stdio", "http", "public"], tier: "destructive", description: "Revoke one of your agent grants. Idempotent — revoking an already-revoked grant returns the same row. Revocation takes effect on the next request (within 60s). Session-auth only.", workflows: "infrastructure", vernacular_kind: "specialist", vernacular_reason: "Destructive operation; invoked by exact name after explicit user confirmation in a specialist flow.", inputSchema: { type: "object", properties: REVOKE_AGENT_GRANT_WRITE_PROPERTIES, required: REVOKE_AGENT_GRANT_REQUIRED, }, }, executor: this.revokeGrant.bind(this), }, { definition: { name: "pica_get_agent_activity", scope: "write:agent_identity", audiences: ["stdio", "http", "public"], tier: "read", description: "List agent write records whose grant's identity you own — your own provenance-stamped agent writes, sorted newest first. Optionally narrow by agent_identity_id or agent_grant_id. Paginated (default 50, max 500 per call). Session-auth only. " + "For the grants themselves rather than what they did, use pica_list_my_agent_grants.", 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: GET_AGENT_ACTIVITY_WRITE_PROPERTIES, required: GET_AGENT_ACTIVITY_REQUIRED, }, }, executor: this.getActivity.bind(this), }, ]; } async createIdentity(args) { const displayName = typeof args.display_name === "string" ? args.display_name.trim() : ""; if (!displayName) { return { content: [{ type: "text", text: "Error: display_name is required." }], isError: true, }; } const clientFingerprint = typeof args.client_fingerprint === "string" && args.client_fingerprint.trim() ? args.client_fingerprint.trim() : null; const result = await this.pica.agentIdentity.createIdentity({ displayName, clientFingerprint, }); const data = result?.data ?? result; return { content: [ { type: "text", text: `Agent identity created: "${displayName}" (id: ${data?.id ?? "unknown"}). Issue a grant with pica_issue_agent_grant to start using it.`, }, ], structuredContent: { kind: "pica.card.agent_identity_created", version: 1, data, }, }; } async issueGrant(args) { const agentIdentityId = typeof args.agent_identity_id === "string" ? args.agent_identity_id.trim() : ""; if (!agentIdentityId) { return { content: [ { type: "text", text: "Error: agent_identity_id is required." }, ], isError: true, }; } if (!Array.isArray(args.scopes) || args.scopes.length === 0) { return { content: [ { type: "text", text: "Error: scopes must be a non-empty array of strings.", }, ], isError: true, }; } const scopes = args.scopes.filter((s) => typeof s === "string"); if (scopes.length !== args.scopes.length) { return { content: [{ type: "text", text: "Error: scopes must be strings." }], isError: true, }; } const expiresAt = typeof args.expires_at === "string" && args.expires_at.trim() ? args.expires_at : null; const result = await this.pica.agentIdentity.issueGrant({ agentIdentityId, scopes, expiresAt, }); const data = result?.data ?? result; // The plaintext token is in data.grant_token and is shown once. // Surface it prominently in the text block so the caller (and // through it the user) sees it without having to drill into the // structured card. return { content: [ { type: "text", text: `Grant issued (id: ${data?.id ?? "unknown"}). ` + `Expires: ${data?.expires_at ?? "n/a"}. ` + `Scopes: ${scopes.join(", ")}.\n\n` + `grant_token: ${data?.grant_token ?? ""}\n\n` + `Save this token now — it will not be shown again. ` + `Present it as Authorization: Bearer <token> on MCP requests.`, }, ], structuredContent: { kind: "pica.card.agent_grant_issued", version: 1, data, }, }; } async listMyGrants(_args) { const result = await this.pica.agentIdentity.listMyGrants(); const data = result?.data ?? result; const rows = Array.isArray(data) ? data : []; const lines = rows.length ? rows .map((row) => { const name = row?.identity?.display_name ?? "unnamed"; const scopes = Array.isArray(row?.scopes) ? row.scopes.join(", ") : ""; const expiry = row?.expires_at ? `expires ${row.expires_at}` : "no expiry"; const revoked = row?.revoked_at ? " [revoked]" : ""; return `- ${name} (${row?.id ?? "unknown"}): ${scopes}; ${expiry}${revoked}`; }) .join("\n") : "(no grants)"; return { content: [ { type: "text", text: `Your agent grants (${rows.length}):\n${lines}`, }, ], structuredContent: { kind: "pica.card.agent_grants_list", version: 1, data: rows, }, }; } async revokeGrant(args) { const grantId = typeof args.grant_id === "string" ? args.grant_id.trim() : ""; if (!grantId) { return { content: [{ type: "text", text: "Error: grant_id is required." }], isError: true, }; } const result = await this.pica.agentIdentity.revokeGrant({ grantId }); const data = result?.data ?? result; return { content: [ { type: "text", text: `Grant ${data?.id ?? grantId} revoked at ${data?.revoked_at ?? "now"}. ` + `The next request using this token will be refused with 401 ` + `within 60 seconds.`, }, ], structuredContent: { kind: "pica.card.agent_grant_revoked", version: 1, data, }, }; } async getActivity(args) { const agentIdentityId = typeof args.agent_identity_id === "string" && args.agent_identity_id.trim() ? args.agent_identity_id.trim() : null; const agentGrantId = typeof args.agent_grant_id === "string" && args.agent_grant_id.trim() ? args.agent_grant_id.trim() : null; const limit = typeof args.limit === "number" ? args.limit : undefined; const offset = typeof args.offset === "number" ? args.offset : undefined; const result = await this.pica.agentIdentity.getActivity({ agentIdentityId, agentGrantId, limit, offset, }); const data = result?.data ?? result; const rows = Array.isArray(data) ? data : []; const lines = rows.length ? rows .map((row) => { const name = row?.grant?.identity?.display_name ?? "unnamed"; const action = row?.action ?? "?"; const table = row?.target_table ?? "?"; const tool = row?.tool_name ?? "?"; const when = row?.created_at ?? ""; return `- ${when} ${name} ${action} ${table} via ${tool}`; }) .join("\n") : "(no activity yet — provenance is stamped once Part 3 wraps write tools)"; return { content: [ { type: "text", text: `Your agent activity (${rows.length}):\n${lines}`, }, ], structuredContent: { kind: "pica.card.agent_activity", version: 1, data: rows, }, }; } } //# sourceMappingURL=agent-identity.js.map