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