@withpica/mcp-server
Version:
MCP Server for withPICA — enables AI assistants to interact with your withPICA music catalog
149 lines • 6.13 kB
JavaScript
// Copyright (c) 2025-2026 Withpica Ltd. All rights reserved.
import { SUBMIT_FEEDBACK_WRITE_PROPERTIES, SUBMIT_FEEDBACK_REQUIRED, } from "@withpica/mcp-utils";
const VALID_CATEGORIES = ["bug_report", "feature_request", "question", "other"];
const ROUTING = {
bug_report: "devops",
feature_request: "ceo",
question: "comms",
other: "category-mapped",
};
export class FeedbackTools {
pica;
constructor(pica) {
this.pica = pica;
}
getTools() {
return [
{
definition: {
name: "pica_submit_feedback",
scope: "write:feedback",
audiences: ["stdio", "http", "public"],
tier: "write",
description: "File a bug report, feature request, question, or general note " +
"without leaving the conversation. Routes to the correct team " +
"role (bug_report→devops, feature_request→ceo, question→comms, " +
"other→category-mapped) and creates a team_tasks row the " +
"founder reviews directly. Subject is always " +
"the authenticated user; no user_id parameter accepted.",
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: SUBMIT_FEEDBACK_WRITE_PROPERTIES,
required: SUBMIT_FEEDBACK_REQUIRED,
},
},
executor: this.submitFeedback.bind(this),
},
];
}
async submitFeedback(args) {
const category = args.category;
const description = args.description;
if (!category) {
return {
content: [{ type: "text", text: "Error: category is required." }],
isError: true,
};
}
if (!VALID_CATEGORIES.includes(category)) {
return {
content: [
{
type: "text",
text: `Error: category must be one of ${VALID_CATEGORIES.join(", ")}.`,
},
],
isError: true,
};
}
if (typeof description !== "string" || description.trim().length === 0) {
return {
content: [{ type: "text", text: "Error: description is required." }],
isError: true,
};
}
// ADR-184 rule 2: reject caller-supplied user_id defensively. The
// subject is always the authenticated session/API-key user.
const sentiment = args.sentiment === "positive" || args.sentiment === "negative"
? args.sentiment
: category === "bug_report"
? "negative"
: "positive";
try {
const result = await this.pica.feedback.submit({
actionType: `general_${category}`,
sentiment,
comment: description.trim(),
portal: "mcp",
});
const data = result?.data || result;
const id = data?.id ?? "unknown";
const routed = ROUTING[category];
return {
content: [
{
type: "text",
text: `Feedback submitted as ${category.replace("_", " ")}; ` +
`routed to ${routed} team. Reference ID: ${id}.`,
},
],
structuredContent: {
kind: "pica.card.feedback_submitted",
version: 1,
data: {
id,
category,
routed_to: routed,
sentiment,
},
},
};
}
catch (e) {
// Surface the underlying error to the agent and to the audit log.
// Prior behaviour returned a generic neutral string which caused
// mcp_audit_log rows with error_code=null / error_message=null,
// making triage impossible (ops_issue 808e5fa0, 2026-05-15).
//
// We still report to ops_issues (fire-and-forget) for visibility,
// but the agent now receives the real error message so it can
// decide whether to retry, adjust the payload, or escalate.
//
// structuredContent.error_code + .error_message are read by the
// audit-logger (index.ts:1891-1895) to populate mcp_audit_log.
const msg = e instanceof Error ? e.message : String(e);
const status = e?.status;
const errorCode = status ? `HTTP_${status}` : "UNKNOWN";
void this.pica.opsIssues
.reportIssue({
kind: "tool_failure",
summary: `pica_submit_feedback failed${status ? ` (${status})` : ""}: ${msg}`,
context: {
tool_name: "pica_submit_feedback",
error_code: errorCode,
parameters_shape: Object.keys(args).sort().join(","),
},
})
.catch(() => {
// never block on the ops report — it's fire-and-forget
});
return {
content: [
{
type: "text",
text: `Error submitting feedback: ${msg}`,
},
],
structuredContent: {
error_code: errorCode,
error_message: msg,
},
isError: true,
};
}
}
}
//# sourceMappingURL=feedback.js.map