@withpica/mcp-server-directory 1.4.2 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +48 -0
- package/README.md +4 -3
- package/dist/client.d.ts +30 -1
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +28 -1
- package/dist/client.js.map +1 -1
- package/dist/config.d.ts +6 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +8 -1
- package/dist/config.js.map +1 -1
- package/dist/lib/changelog.generated.d.ts +2 -2
- package/dist/lib/changelog.generated.d.ts.map +1 -1
- package/dist/lib/changelog.generated.js +2 -2
- package/dist/lib/changelog.generated.js.map +1 -1
- package/dist/prompts/index.js +1 -1
- package/dist/prompts/index.js.map +1 -1
- package/dist/prompts/public-question-atlas.d.ts.map +1 -1
- package/dist/prompts/public-question-atlas.js +17 -0
- package/dist/prompts/public-question-atlas.js.map +1 -1
- package/dist/resources/llms-primer.d.ts +1 -1
- package/dist/resources/llms-primer.d.ts.map +1 -1
- package/dist/resources/llms-primer.js +6 -2
- package/dist/resources/llms-primer.js.map +1 -1
- package/dist/server.d.ts +9 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +15 -5
- package/dist/server.js.map +1 -1
- package/dist/skills/skills.generated.js +1 -1
- package/dist/skills/skills.generated.js.map +1 -1
- package/dist/tools/chain.d.ts +2 -2
- package/dist/tools/chain.d.ts.map +1 -1
- package/dist/tools/chain.js +15 -0
- package/dist/tools/chain.js.map +1 -1
- package/dist/tools/enquiries.d.ts +19 -0
- package/dist/tools/enquiries.d.ts.map +1 -0
- package/dist/tools/enquiries.js +190 -0
- package/dist/tools/enquiries.js.map +1 -0
- package/dist/tools/index.d.ts +6 -5
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +2 -0
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/people.d.ts +2 -2
- package/dist/tools/people.d.ts.map +1 -1
- package/dist/tools/people.js.map +1 -1
- package/dist/tools/recordings.d.ts +2 -2
- package/dist/tools/recordings.d.ts.map +1 -1
- package/dist/tools/recordings.js.map +1 -1
- package/dist/tools/release-notes.d.ts +2 -2
- package/dist/tools/release-notes.d.ts.map +1 -1
- package/dist/tools/release-notes.js.map +1 -1
- package/dist/tools/search.d.ts +2 -2
- package/dist/tools/search.d.ts.map +1 -1
- package/dist/tools/search.js.map +1 -1
- package/dist/tools/works.d.ts +2 -2
- package/dist/tools/works.d.ts.map +1 -1
- package/dist/tools/works.js +17 -1
- package/dist/tools/works.js.map +1 -1
- package/dist/utils/errors.d.ts +10 -0
- package/dist/utils/errors.d.ts.map +1 -1
- package/dist/utils/errors.js +33 -0
- package/dist/utils/errors.js.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/prompts/prompt-eval-harness.test.ts +3 -2
- package/src/__tests__/tools/chain.test.ts +34 -0
- package/src/__tests__/tools/composability-chains.test.ts +2 -2
- package/src/__tests__/tools/enquiries.test.ts +135 -0
- package/src/__tests__/tools/works.test.ts +3 -0
- package/src/__tests__/utils/errors.test.ts +13 -0
- package/src/client.ts +53 -2
- package/src/config.ts +12 -1
- package/src/prompts/index.ts +1 -1
- package/src/prompts/public-question-atlas.ts +18 -0
- package/src/resources/llms-primer.ts +6 -2
- package/src/server.ts +19 -7
- package/src/skills/find-music-for-sync-brief/SKILL.md +1 -1
- package/src/skills/skills.generated.ts +1 -1
- package/src/tools/chain.ts +18 -3
- package/src/tools/enquiries.ts +210 -0
- package/src/tools/index.ts +8 -5
- package/src/tools/people.ts +3 -3
- package/src/tools/recordings.ts +3 -3
- package/src/tools/release-notes.ts +2 -2
- package/src/tools/search.ts +3 -3
- package/src/tools/works.ts +22 -4
- package/src/utils/errors.ts +40 -0
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
// Copyright (c) 2025-2026 Withpica Ltd. All rights reserved.
|
|
2
|
+
|
|
3
|
+
import type { DirectoryApi } from "../client.js";
|
|
4
|
+
import { ToolDefinition, ToolExecutor, ToolResult } from "./index.js";
|
|
5
|
+
|
|
6
|
+
const PROJECT_TYPES = [
|
|
7
|
+
"film",
|
|
8
|
+
"tv series",
|
|
9
|
+
"commercial",
|
|
10
|
+
"trailer",
|
|
11
|
+
"video game",
|
|
12
|
+
"podcast",
|
|
13
|
+
"social media",
|
|
14
|
+
"other",
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
/** The enquiry route answers `{ error: "…" }` or `{ error: { message } }`. */
|
|
18
|
+
function errorMessage(json: any): string | null {
|
|
19
|
+
const e = json?.error;
|
|
20
|
+
if (typeof e === "string") return e;
|
|
21
|
+
if (e && typeof e.message === "string") return e.message;
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* The route stores the enquiry and THEN sends the emails, so a 5xx or a lost
|
|
27
|
+
* answer can come after it was stored. Saying "not sent" there would invite a
|
|
28
|
+
* resend, and a second enquiry and email to the owner.
|
|
29
|
+
*/
|
|
30
|
+
function outcomeUnknown(detail: string): ToolResult {
|
|
31
|
+
const reason =
|
|
32
|
+
"Outcome unknown: the enquiry may already have reached the owner. " +
|
|
33
|
+
"Do not resend without checking with the user first.";
|
|
34
|
+
return {
|
|
35
|
+
content: [{ type: "text", text: `${reason} (${detail})` }],
|
|
36
|
+
structuredContent: { sent: null, outcome: "unknown", reason, detail },
|
|
37
|
+
isError: true,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* The directory's only non-read tool: send a licensing enquiry to the owner
|
|
43
|
+
* of a work listed in the public directory. It goes through the same public
|
|
44
|
+
* route as the directory's web form (POST /api/shop/license-enquiries), so
|
|
45
|
+
* the same rules apply: the work must be listed, the owner must not have
|
|
46
|
+
* switched enquiries off, and a budget is optional.
|
|
47
|
+
*/
|
|
48
|
+
export class DirectoryEnquiryTools {
|
|
49
|
+
private client: DirectoryApi;
|
|
50
|
+
|
|
51
|
+
constructor(client: DirectoryApi) {
|
|
52
|
+
this.client = client;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
getTools(): Array<{ definition: ToolDefinition; executor: ToolExecutor }> {
|
|
56
|
+
return [
|
|
57
|
+
{
|
|
58
|
+
definition: {
|
|
59
|
+
name: "directory_send_licensing_enquiry",
|
|
60
|
+
tier: "write",
|
|
61
|
+
description:
|
|
62
|
+
"Use when the user asks: 'ask the owner if I can license this', " +
|
|
63
|
+
"'send a licensing enquiry for X', 'contact the rights holder about using this song'. " +
|
|
64
|
+
"Sends a licensing enquiry to the owner of a work listed in the public directory. " +
|
|
65
|
+
"The enquiry is passed to the owner, who can reply to the user by email (the user's address is " +
|
|
66
|
+
"shared with them); a confirmation email to the user is usually sent but not guaranteed. " +
|
|
67
|
+
"Nothing is agreed or priced: it only opens the conversation. " +
|
|
68
|
+
"ONLY call after the user has explicitly asked to send it and has given their own name, " +
|
|
69
|
+
"email and a description of the project. Never invent these, and never send on their behalf " +
|
|
70
|
+
"without asking. Get work_id from directory_lookup_work, directory_list_works or directory_chain. " +
|
|
71
|
+
"A 403 means the owner is not taking enquiries; a 404 means the work is not publicly listed; " +
|
|
72
|
+
"an unknown outcome means it may already have been sent, so never resend without asking. " +
|
|
73
|
+
"NOT FOR: looking up who owns a song (use directory_lookup_work). " +
|
|
74
|
+
"→ then: directory_lookup_work (the work page and credits, to share with the user)",
|
|
75
|
+
inputSchema: {
|
|
76
|
+
type: "object",
|
|
77
|
+
properties: {
|
|
78
|
+
work_id: {
|
|
79
|
+
type: "string",
|
|
80
|
+
description: "The work's UUID from the directory.",
|
|
81
|
+
},
|
|
82
|
+
contact_name: {
|
|
83
|
+
type: "string",
|
|
84
|
+
description: "The user's own name, as they gave it.",
|
|
85
|
+
},
|
|
86
|
+
contact_email: {
|
|
87
|
+
type: "string",
|
|
88
|
+
description:
|
|
89
|
+
"The user's own email address. The owner will reply to it.",
|
|
90
|
+
},
|
|
91
|
+
company_name: {
|
|
92
|
+
type: "string",
|
|
93
|
+
description: "The user's company or production (optional).",
|
|
94
|
+
},
|
|
95
|
+
project_type: {
|
|
96
|
+
type: "string",
|
|
97
|
+
enum: PROJECT_TYPES,
|
|
98
|
+
description: "The kind of project.",
|
|
99
|
+
},
|
|
100
|
+
project_description: {
|
|
101
|
+
type: "string",
|
|
102
|
+
description:
|
|
103
|
+
"How the music would be used, in the user's words.",
|
|
104
|
+
},
|
|
105
|
+
territory: {
|
|
106
|
+
type: "string",
|
|
107
|
+
description:
|
|
108
|
+
"Where it would be used (optional; omit if the user didn't say).",
|
|
109
|
+
},
|
|
110
|
+
term: {
|
|
111
|
+
type: "string",
|
|
112
|
+
description:
|
|
113
|
+
"For how long (optional; omit if the user didn't say).",
|
|
114
|
+
},
|
|
115
|
+
budget_amount: {
|
|
116
|
+
type: "number",
|
|
117
|
+
description:
|
|
118
|
+
"Proposed fee, only if the user named one (optional).",
|
|
119
|
+
},
|
|
120
|
+
budget_currency: {
|
|
121
|
+
type: "string",
|
|
122
|
+
description: "Three-letter currency code (default GBP).",
|
|
123
|
+
},
|
|
124
|
+
budget_notes: {
|
|
125
|
+
type: "string",
|
|
126
|
+
description:
|
|
127
|
+
"Anything the user said about budget that isn't a single amount, e.g. a range.",
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
required: [
|
|
131
|
+
"work_id",
|
|
132
|
+
"contact_name",
|
|
133
|
+
"contact_email",
|
|
134
|
+
"project_type",
|
|
135
|
+
"project_description",
|
|
136
|
+
],
|
|
137
|
+
additionalProperties: false,
|
|
138
|
+
},
|
|
139
|
+
},
|
|
140
|
+
executor: this.sendEnquiry.bind(this),
|
|
141
|
+
},
|
|
142
|
+
];
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
private async sendEnquiry(args: Record<string, any>): Promise<ToolResult> {
|
|
146
|
+
const workId = String(args.work_id ?? "");
|
|
147
|
+
let answer: { status: number; json: any };
|
|
148
|
+
try {
|
|
149
|
+
answer = await this.client.sendLicensingEnquiry({
|
|
150
|
+
workId,
|
|
151
|
+
contactName: args.contact_name,
|
|
152
|
+
contactEmail: args.contact_email,
|
|
153
|
+
companyName: args.company_name,
|
|
154
|
+
projectType: args.project_type,
|
|
155
|
+
projectDescription: args.project_description,
|
|
156
|
+
// Not specified is said plainly, never guessed.
|
|
157
|
+
territory: args.territory || "not specified",
|
|
158
|
+
duration: args.term || "not specified",
|
|
159
|
+
proposedBudgetAmount:
|
|
160
|
+
typeof args.budget_amount === "number" ? args.budget_amount : null,
|
|
161
|
+
proposedBudgetCurrency: args.budget_currency || "GBP",
|
|
162
|
+
budgetNotes: args.budget_notes,
|
|
163
|
+
});
|
|
164
|
+
} catch (err) {
|
|
165
|
+
// No answer at all (timeout, network): the route may still have stored it.
|
|
166
|
+
return outcomeUnknown((err as Error).message);
|
|
167
|
+
}
|
|
168
|
+
const { status, json } = answer;
|
|
169
|
+
const page = this.client.workPageUrl(workId);
|
|
170
|
+
|
|
171
|
+
if (status >= 200 && status < 300 && json?.data?.id) {
|
|
172
|
+
return {
|
|
173
|
+
content: [
|
|
174
|
+
{
|
|
175
|
+
type: "text",
|
|
176
|
+
text:
|
|
177
|
+
`Enquiry passed to the owner (reference ${json.data.id}). ` +
|
|
178
|
+
`They can reply to ${args.contact_email} if they want to take it further; ` +
|
|
179
|
+
`nothing has been agreed. Work page: ${page}`,
|
|
180
|
+
},
|
|
181
|
+
],
|
|
182
|
+
structuredContent: {
|
|
183
|
+
sent: true,
|
|
184
|
+
enquiry_id: json.data.id,
|
|
185
|
+
status: json.data.status,
|
|
186
|
+
work_page_url: page,
|
|
187
|
+
},
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// A 5xx, or a 2xx without the reference, may have stored the enquiry.
|
|
192
|
+
if (status >= 500 || (status >= 200 && status < 300))
|
|
193
|
+
return outcomeUnknown(`HTTP ${status}`);
|
|
194
|
+
|
|
195
|
+
const reason =
|
|
196
|
+
status === 403
|
|
197
|
+
? "The owner is not taking licensing enquiries for this work."
|
|
198
|
+
: status === 404
|
|
199
|
+
? "That work is not listed in the public directory."
|
|
200
|
+
: status === 429
|
|
201
|
+
? "Too many enquiries were sent recently. Try again later."
|
|
202
|
+
: (errorMessage(json) ??
|
|
203
|
+
`The enquiry was not sent (HTTP ${status}).`);
|
|
204
|
+
return {
|
|
205
|
+
content: [{ type: "text", text: `Not sent. ${reason}` }],
|
|
206
|
+
structuredContent: { sent: false, http_status: status, reason },
|
|
207
|
+
isError: true,
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
}
|
package/src/tools/index.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Copyright (c) 2025-2026 Withpica Ltd. All rights reserved.
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import type { DirectoryApi } from "../client.js";
|
|
4
4
|
import { DirectoryWorksTools } from "./works.js";
|
|
5
5
|
import { DirectoryPeopleTools } from "./people.js";
|
|
6
6
|
import { DirectorySearchTools } from "./search.js";
|
|
@@ -8,13 +8,15 @@ import { DirectoryRecordingsTools } from "./recordings.js";
|
|
|
8
8
|
import { DirectoryChainTools } from "./chain.js";
|
|
9
9
|
import { DirectorySkillsTools } from "./skills.js";
|
|
10
10
|
import { DirectoryReleaseNotesTools } from "./release-notes.js";
|
|
11
|
+
import { DirectoryEnquiryTools } from "./enquiries.js";
|
|
11
12
|
import { formatError, logError, ToolExecutionError } from "../utils/errors.js";
|
|
12
13
|
|
|
13
14
|
/**
|
|
14
15
|
* ADR-230 — authority tier on every directory_* tool. Sister of the
|
|
15
|
-
* creator + team MCP's `Tier` declaration. Every directory tool is
|
|
16
|
-
*
|
|
17
|
-
*
|
|
16
|
+
* creator + team MCP's `Tier` declaration. Every directory tool is `read`
|
|
17
|
+
* (public catalogue lookup) except directory_send_licensing_enquiry
|
|
18
|
+
* (`write`, tools/enquiries.ts), which sends an enquiry through the same
|
|
19
|
+
* public route as the directory's web form.
|
|
18
20
|
*/
|
|
19
21
|
export type Tier = "read" | "draft" | "write" | "destructive";
|
|
20
22
|
|
|
@@ -45,7 +47,7 @@ export class ToolRegistry {
|
|
|
45
47
|
{ definition: ToolDefinition; executor: ToolExecutor }
|
|
46
48
|
>;
|
|
47
49
|
|
|
48
|
-
constructor(client:
|
|
50
|
+
constructor(client: DirectoryApi) {
|
|
49
51
|
this.tools = new Map();
|
|
50
52
|
|
|
51
53
|
const groups = [
|
|
@@ -56,6 +58,7 @@ export class ToolRegistry {
|
|
|
56
58
|
new DirectoryChainTools(client),
|
|
57
59
|
new DirectorySkillsTools(),
|
|
58
60
|
new DirectoryReleaseNotesTools(client),
|
|
61
|
+
new DirectoryEnquiryTools(client),
|
|
59
62
|
];
|
|
60
63
|
|
|
61
64
|
for (const group of groups) {
|
package/src/tools/people.ts
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
// Copyright (c) 2025-2026 Withpica Ltd. All rights reserved.
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import type { DirectoryApi } from "../client.js";
|
|
4
4
|
import { ToolDefinition, ToolExecutor } from "./index.js";
|
|
5
5
|
import { formatStructuredList } from "../utils/formatting.js";
|
|
6
6
|
|
|
7
7
|
export class DirectoryPeopleTools {
|
|
8
|
-
private client:
|
|
8
|
+
private client: DirectoryApi;
|
|
9
9
|
|
|
10
|
-
constructor(client:
|
|
10
|
+
constructor(client: DirectoryApi) {
|
|
11
11
|
this.client = client;
|
|
12
12
|
}
|
|
13
13
|
|
package/src/tools/recordings.ts
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
// Copyright (c) 2025-2026 Withpica Ltd. All rights reserved.
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import type { DirectoryApi } from "../client.js";
|
|
4
4
|
import { ToolDefinition, ToolExecutor } from "./index.js";
|
|
5
5
|
import { formatStructuredList } from "../utils/formatting.js";
|
|
6
6
|
|
|
7
7
|
export class DirectoryRecordingsTools {
|
|
8
|
-
private client:
|
|
8
|
+
private client: DirectoryApi;
|
|
9
9
|
|
|
10
|
-
constructor(client:
|
|
10
|
+
constructor(client: DirectoryApi) {
|
|
11
11
|
this.client = client;
|
|
12
12
|
}
|
|
13
13
|
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* shipped in the npm tarball, already vetted as public.
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
|
-
import {
|
|
16
|
+
import type { DirectoryApi } from "../client.js";
|
|
17
17
|
import { ToolDefinition, ToolExecutor, ToolResult } from "./index.js";
|
|
18
18
|
import {
|
|
19
19
|
parseChangelogText,
|
|
@@ -40,7 +40,7 @@ function compareSemver(a: string, b: string): number {
|
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
export class DirectoryReleaseNotesTools {
|
|
43
|
-
constructor(private client:
|
|
43
|
+
constructor(private client: DirectoryApi) {}
|
|
44
44
|
|
|
45
45
|
getTools(): Array<{ definition: ToolDefinition; executor: ToolExecutor }> {
|
|
46
46
|
return [
|
package/src/tools/search.ts
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
// Copyright (c) 2025-2026 Withpica Ltd. All rights reserved.
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import type { DirectoryApi } from "../client.js";
|
|
4
4
|
import { ToolDefinition, ToolExecutor } from "./index.js";
|
|
5
5
|
import { formatStructuredList } from "../utils/formatting.js";
|
|
6
6
|
|
|
7
7
|
export class DirectorySearchTools {
|
|
8
|
-
private client:
|
|
8
|
+
private client: DirectoryApi;
|
|
9
9
|
|
|
10
|
-
constructor(client:
|
|
10
|
+
constructor(client: DirectoryApi) {
|
|
11
11
|
this.client = client;
|
|
12
12
|
}
|
|
13
13
|
|
package/src/tools/works.ts
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
// Copyright (c) 2025-2026 Withpica Ltd. All rights reserved.
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import type { DirectoryApi } from "../client.js";
|
|
4
4
|
import { ToolDefinition, ToolExecutor } from "./index.js";
|
|
5
5
|
import { formatStructuredList } from "../utils/formatting.js";
|
|
6
6
|
|
|
7
7
|
export class DirectoryWorksTools {
|
|
8
|
-
private client:
|
|
8
|
+
private client: DirectoryApi;
|
|
9
9
|
|
|
10
|
-
constructor(client:
|
|
10
|
+
constructor(client: DirectoryApi) {
|
|
11
11
|
this.client = client;
|
|
12
12
|
}
|
|
13
13
|
|
|
@@ -243,11 +243,29 @@ export class DirectoryWorksTools {
|
|
|
243
243
|
lines.push("");
|
|
244
244
|
}
|
|
245
245
|
|
|
246
|
+
// How to reach the owner. Enquiries are opt-out (only an explicit false
|
|
247
|
+
// closes them), the same test the enquiry route applies.
|
|
248
|
+
const workId: string | undefined = data.work_id || data.id;
|
|
249
|
+
const pageUrl = workId ? this.client.workPageUrl(workId) : undefined;
|
|
250
|
+
const enquiriesOpen = data.contact_enabled !== false;
|
|
251
|
+
lines.push("### Licensing");
|
|
252
|
+
if (pageUrl) lines.push(`- Work page: ${pageUrl}`);
|
|
253
|
+
lines.push(
|
|
254
|
+
enquiriesOpen && workId
|
|
255
|
+
? `- Enquiries open: with the user's go-ahead, send one via directory_send_licensing_enquiry (work_id ${workId}).`
|
|
256
|
+
: "- The owner is not taking licensing enquiries for this work.",
|
|
257
|
+
);
|
|
258
|
+
lines.push("");
|
|
259
|
+
|
|
246
260
|
const markdown = lines.join("\n");
|
|
247
261
|
|
|
248
262
|
return {
|
|
249
263
|
content: [{ type: "text", text: markdown }],
|
|
250
|
-
structuredContent:
|
|
264
|
+
structuredContent: {
|
|
265
|
+
...(data as Record<string, unknown>),
|
|
266
|
+
work_page_url: pageUrl ?? null,
|
|
267
|
+
enquiries_open: enquiriesOpen && Boolean(workId),
|
|
268
|
+
},
|
|
251
269
|
};
|
|
252
270
|
}
|
|
253
271
|
|
package/src/utils/errors.ts
CHANGED
|
@@ -19,6 +19,24 @@ export class ToolExecutionError extends McpServerError {
|
|
|
19
19
|
}
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
/**
|
|
23
|
+
* Status → code for a route refusal that carried no code of its own.
|
|
24
|
+
*
|
|
25
|
+
* ⚠️ A port of `STATUS_REFUSAL_CODES` in
|
|
26
|
+
* `mcp-server-shared/mcp-utils/src/errors.ts`. This package does not depend on
|
|
27
|
+
* `@withpica/mcp-utils` (it publishes on its own), so it cannot import the
|
|
28
|
+
* shared formatter; `scripts/__tests__/format-error-status-parity.test.ts`
|
|
29
|
+
* fails if the two tables disagree.
|
|
30
|
+
*/
|
|
31
|
+
export const STATUS_REFUSAL_CODES: Record<number, string> = {
|
|
32
|
+
400: "VALIDATION_ERROR",
|
|
33
|
+
404: "NOT_FOUND",
|
|
34
|
+
409: "CONFLICT",
|
|
35
|
+
413: "PAYLOAD_TOO_LARGE",
|
|
36
|
+
415: "UNSUPPORTED_MEDIA_TYPE",
|
|
37
|
+
422: "VALIDATION_ERROR",
|
|
38
|
+
};
|
|
39
|
+
|
|
22
40
|
export function formatError(error: any): { type: string; text: string } {
|
|
23
41
|
if (error instanceof McpServerError) {
|
|
24
42
|
return {
|
|
@@ -78,6 +96,28 @@ export function formatError(error: any): { type: string; text: string } {
|
|
|
78
96
|
};
|
|
79
97
|
}
|
|
80
98
|
|
|
99
|
+
// A code-less 4xx is still a refusal; its status names the kind. Without
|
|
100
|
+
// this it read as UNKNOWN_ERROR, the bucket for an unclassified crash (ops
|
|
101
|
+
// issue 55510879 — the same defect in the shared formatter).
|
|
102
|
+
const status = (error as { status?: unknown })?.status;
|
|
103
|
+
const statusCode =
|
|
104
|
+
typeof status === "number" ? STATUS_REFUSAL_CODES[status] : undefined;
|
|
105
|
+
if (statusCode) {
|
|
106
|
+
return {
|
|
107
|
+
type: "text",
|
|
108
|
+
text: JSON.stringify(
|
|
109
|
+
{
|
|
110
|
+
error: statusCode,
|
|
111
|
+
message: bodyMessage || rawMessage,
|
|
112
|
+
status,
|
|
113
|
+
...(bodyDetails !== undefined ? { details: bodyDetails } : {}),
|
|
114
|
+
},
|
|
115
|
+
null,
|
|
116
|
+
2,
|
|
117
|
+
),
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
81
121
|
return {
|
|
82
122
|
type: "text",
|
|
83
123
|
text: JSON.stringify(
|