@promptlayer/mcp-server 1.2.0 → 1.3.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/CLAUDE.md +1 -6
- package/build/client.d.ts +2 -1
- package/build/client.d.ts.map +1 -1
- package/build/client.js +2 -1
- package/build/client.js.map +1 -1
- package/build/handlers.d.ts.map +1 -1
- package/build/handlers.js +2 -1
- package/build/handlers.js.map +1 -1
- package/build/types.d.ts +40 -19
- package/build/types.d.ts.map +1 -1
- package/build/types.js +15 -5
- package/build/types.js.map +1 -1
- package/gcp/src/index.ts +4 -2
- package/package.json +3 -8
- package/src/client.ts +2 -1
- package/src/handlers.ts +5 -2
- package/src/types.ts +17 -5
- package/scripts/diff-endpoints.ts +0 -277
- package/scripts/extract-mcp-tools.ts +0 -122
- package/scripts/fetch-openapi-endpoints.ts +0 -150
- package/scripts/show-descriptions.ts +0 -101
package/src/types.ts
CHANGED
|
@@ -228,9 +228,6 @@ export const CreateDatasetVersionFromFileArgsSchema = z.object({
|
|
|
228
228
|
export const CreateDatasetVersionFromFilterParamsArgsSchema = z.object({
|
|
229
229
|
dataset_group_id: z.number().int().describe("Dataset group ID"),
|
|
230
230
|
variables_to_parse: z.array(z.string()).optional().describe("Variables to extract from request logs"),
|
|
231
|
-
prompt_id: z.number().int().optional().describe("Filter by prompt template ID"),
|
|
232
|
-
prompt_version_id: z.number().int().optional().describe("Filter by prompt version ID"),
|
|
233
|
-
prompt_label_id: z.number().int().optional().describe("Filter by prompt label ID"),
|
|
234
231
|
workspace_id: z.number().int().optional().describe("Workspace ID"),
|
|
235
232
|
tags: z.array(z.string()).optional().describe("Filter by tags (simple tag filter)"),
|
|
236
233
|
metadata: z.record(z.string()).optional().describe("Simple metadata key-value filter"),
|
|
@@ -251,7 +248,7 @@ export const CreateDatasetVersionFromFilterParamsArgsSchema = z.object({
|
|
|
251
248
|
name: z.string().describe("Prompt template name"),
|
|
252
249
|
version_numbers: z.array(z.number().int()).optional().describe("Filter to specific version numbers"),
|
|
253
250
|
labels: z.array(z.string()).optional().describe("Filter to specific labels"),
|
|
254
|
-
})).optional().describe("Include request logs matching these prompt templates"),
|
|
251
|
+
})).optional().describe("Include request logs matching these prompt templates. This is the primary way to filter by prompt — use the prompt name (not ID)."),
|
|
255
252
|
prompt_templates_exclude: z.array(z.object({
|
|
256
253
|
name: z.string().describe("Prompt template name"),
|
|
257
254
|
version_numbers: z.array(z.number().int()).optional().describe("Filter to specific version numbers"),
|
|
@@ -423,6 +420,15 @@ export const GetWorkflowVersionExecutionResultsArgsSchema = z.object({
|
|
|
423
420
|
// ── Get Agent (GET /workflows/{workflow_id_or_name}) ─────────────────────
|
|
424
421
|
|
|
425
422
|
export const GetWorkflowArgsSchema = z.object({
|
|
423
|
+
workflow_id_or_name: z.string().describe("Agent ID or name"),
|
|
424
|
+
version: z.number().int().optional().describe("Specific version number (mutually exclusive with label)"),
|
|
425
|
+
label: z.string().optional().describe("Release label name, e.g. 'prod' (mutually exclusive with version)"),
|
|
426
|
+
api_key: z.string().optional().describe("PromptLayer API key (optional, defaults to PROMPTLAYER_API_KEY env var)"),
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
// ── Get Agent Labels (GET /workflows/{workflow_id_or_name}/labels) ────────
|
|
430
|
+
|
|
431
|
+
export const GetWorkflowLabelsArgsSchema = z.object({
|
|
426
432
|
workflow_id_or_name: z.string().describe("Agent ID or name"),
|
|
427
433
|
api_key: z.string().optional().describe("PromptLayer API key (optional, defaults to PROMPTLAYER_API_KEY env var)"),
|
|
428
434
|
});
|
|
@@ -807,10 +813,16 @@ export const TOOL_DEFINITIONS = {
|
|
|
807
813
|
},
|
|
808
814
|
"get-workflow": {
|
|
809
815
|
name: "get-workflow",
|
|
810
|
-
description: "Get a single agent (called 'workflow' in the API) by ID or name. Returns the agent details and
|
|
816
|
+
description: "Get a single agent (called 'workflow' in the API) by ID or name. Returns the agent details including full node configuration, edges, and version info. Optionally filter by version number or release label.",
|
|
811
817
|
inputSchema: GetWorkflowArgsSchema,
|
|
812
818
|
annotations: { readOnlyHint: true },
|
|
813
819
|
},
|
|
820
|
+
"get-workflow-labels": {
|
|
821
|
+
name: "get-workflow-labels",
|
|
822
|
+
description: "List all release labels for an agent (workflow). Returns each label with its name, ID, and the version it points to.",
|
|
823
|
+
inputSchema: GetWorkflowLabelsArgsSchema,
|
|
824
|
+
annotations: { readOnlyHint: true },
|
|
825
|
+
},
|
|
814
826
|
|
|
815
827
|
// ── Folders ─────────────────────────────────────────────────────────
|
|
816
828
|
"create-folder": {
|
|
@@ -1,277 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Compares openapi-endpoints.json (source of truth) against mcp-tools.json
|
|
3
|
-
* (what our MCP implements) and reports discrepancies.
|
|
4
|
-
*
|
|
5
|
-
* Usage: npx tsx scripts/diff-endpoints.ts
|
|
6
|
-
*
|
|
7
|
-
* Exit code 0 = in sync (or all issues are known exceptions), 1 = new issues found.
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import { readFileSync } from "fs";
|
|
11
|
-
|
|
12
|
-
const OPENAPI_PATH = new URL("./openapi-endpoints.json", import.meta.url).pathname;
|
|
13
|
-
const MCP_PATH = new URL("./mcp-tools.json", import.meta.url).pathname;
|
|
14
|
-
|
|
15
|
-
// ─── Known exceptions ────────────────────────────────────────────────────────
|
|
16
|
-
// Each string is matched exactly against an issue line. When ALL remaining
|
|
17
|
-
// issues are in this list, the script exits 0 (pass). Add a comment for each
|
|
18
|
-
// explaining WHY it's expected so a future reader/agent understands.
|
|
19
|
-
const KNOWN_EXCEPTIONS: string[] = [
|
|
20
|
-
// update-report-score-card is documented in the PromptLayer reference text
|
|
21
|
-
// (https://docs.promptlayer.com/reference/update-report-score-card) but the
|
|
22
|
-
// endpoint is not present in the OpenAPI spec (openapi.json). We implement
|
|
23
|
-
// it because users need it; it will resolve when PromptLayer adds it to the spec.
|
|
24
|
-
"EXTRA IN MCP (not in OpenAPI): PATCH /reports/{report_id}/score-card (tool: update-report-score-card)",
|
|
25
|
-
|
|
26
|
-
// These endpoints exist in the OpenAPI spec but were intentionally removed from the MCP server.
|
|
27
|
-
// track-prompt, track-score, track-metadata, and track-group are redundant with log-request
|
|
28
|
-
// which accepts all tracking fields inline. add-report-column is removed as well.
|
|
29
|
-
"MISSING IN MCP: POST /rest/track-prompt (Track Prompt)",
|
|
30
|
-
"MISSING IN MCP: POST /rest/track-score (Track Score)",
|
|
31
|
-
"MISSING IN MCP: POST /rest/track-metadata (Track Metadata)",
|
|
32
|
-
"MISSING IN MCP: POST /rest/track-group (Track Group)",
|
|
33
|
-
"MISSING IN MCP: POST /report-columns (Add Column to Evaluation Pipeline)",
|
|
34
|
-
|
|
35
|
-
// get-workflow exists in the backend (public_api/workflows/get_workflows_id_or_name.py)
|
|
36
|
-
// but is not in the OpenAPI spec.
|
|
37
|
-
"EXTRA IN MCP (not in OpenAPI): GET /workflows/{workflow_id_or_name} (tool: get-workflow)",
|
|
38
|
-
|
|
39
|
-
// Folder management endpoints exist in the backend (public_api/) but are not yet in the OpenAPI spec.
|
|
40
|
-
// edit-folder was added in PR #244.
|
|
41
|
-
"EXTRA IN MCP (not in OpenAPI): PATCH /api/public/v2/folders/{folder_id} (tool: edit-folder)",
|
|
42
|
-
"EXTRA IN MCP (not in OpenAPI): GET /api/public/v2/folders/entities (tool: get-folder-entities)",
|
|
43
|
-
"EXTRA IN MCP (not in OpenAPI): POST /api/public/v2/folders/entities (tool: move-folder-entities)",
|
|
44
|
-
"EXTRA IN MCP (not in OpenAPI): DELETE /api/public/v2/folders/entities (tool: delete-folder-entities)",
|
|
45
|
-
"EXTRA IN MCP (not in OpenAPI): GET /api/public/v2/folders/resolve-id (tool: resolve-folder-id)",
|
|
46
|
-
|
|
47
|
-
// callback_url is documented in the run-workflow reference text
|
|
48
|
-
// (https://docs.promptlayer.com/reference/run-workflow) under Request Body
|
|
49
|
-
// but is not yet in the OpenAPI spec. We include it for async webhook support.
|
|
50
|
-
'EXTRA FIELD: POST /workflows/{workflow_name}/run > "callback_url" not in OpenAPI [tool: run-workflow]',
|
|
51
|
-
|
|
52
|
-
// The OpenAPI spec defines input/output as oneOf (ChatPrompt | CompletionPrompt).
|
|
53
|
-
// We type them as plain object (z.record) because the MCP tool passes them through
|
|
54
|
-
// as-is to the API — the server validates the discriminated union, not us.
|
|
55
|
-
'TYPE MISMATCH: POST /log-request > "input": MCP=object, OpenAPI=oneOf [tool: log-request]',
|
|
56
|
-
'TYPE MISMATCH: POST /log-request > "output": MCP=object, OpenAPI=oneOf [tool: log-request]',
|
|
57
|
-
|
|
58
|
-
// workspace_id is accepted by the backend but not yet in the OpenAPI spec for these endpoints.
|
|
59
|
-
'EXTRA FIELD: GET /prompt-templates > "workspace_id" not in OpenAPI [tool: list-prompt-templates]',
|
|
60
|
-
'EXTRA FIELD: POST /api/public/v2/folders > "workspace_id" not in OpenAPI [tool: create-folder]',
|
|
61
|
-
|
|
62
|
-
// snippet_overrides is accepted by the backend publish endpoint but not in OpenAPI spec.
|
|
63
|
-
'EXTRA FIELD: POST /rest/prompt-templates > "snippet_overrides" not in OpenAPI [tool: publish-prompt-template]',
|
|
64
|
-
|
|
65
|
-
// Backend accepts prompt_version_id as alternative to prompt_version_number for label ops,
|
|
66
|
-
// and we make prompt_version_number optional to support both. Not yet in OpenAPI spec.
|
|
67
|
-
'EXTRA FIELD: POST /prompts/{prompt_id}/label > "prompt_version_id" not in OpenAPI [tool: create-prompt-label]',
|
|
68
|
-
'EXTRA FIELD: PATCH /prompt-labels/{prompt_label_id} > "prompt_version_id" not in OpenAPI [tool: move-prompt-label]',
|
|
69
|
-
'REQUIRED MISMATCH: POST /prompts/{prompt_id}/label > "prompt_version_number": MCP=false, OpenAPI=true [tool: create-prompt-label]',
|
|
70
|
-
'REQUIRED MISMATCH: PATCH /prompt-labels/{prompt_label_id} > "prompt_version_number": MCP=false, OpenAPI=true [tool: move-prompt-label]',
|
|
71
|
-
|
|
72
|
-
// score_name is accepted by the backend log-request endpoint but not in OpenAPI spec.
|
|
73
|
-
'EXTRA FIELD: POST /log-request > "score_name" not in OpenAPI [tool: log-request]',
|
|
74
|
-
|
|
75
|
-
// folder_id is accepted by the backend create-dataset-group endpoint but not in OpenAPI spec.
|
|
76
|
-
'EXTRA FIELD: POST /api/public/v2/dataset-groups > "folder_id" not in OpenAPI [tool: create-dataset-group]',
|
|
77
|
-
|
|
78
|
-
// Backend auto-generates name if omitted for dataset groups; OpenAPI marks it required.
|
|
79
|
-
'REQUIRED MISMATCH: POST /api/public/v2/dataset-groups > "name": MCP=false, OpenAPI=true [tool: create-dataset-group]',
|
|
80
|
-
|
|
81
|
-
// workflow_node_id and workflow_node_name are accepted by the backend execution results
|
|
82
|
-
// endpoint but not in OpenAPI spec.
|
|
83
|
-
'EXTRA FIELD: GET /workflow-version-execution-results > "workflow_node_id" not in OpenAPI [tool: get-workflow-version-execution-results]',
|
|
84
|
-
'EXTRA FIELD: GET /workflow-version-execution-results > "workflow_node_name" not in OpenAPI [tool: get-workflow-version-execution-results]',
|
|
85
|
-
|
|
86
|
-
// The create-dataset-version-from-filter-params endpoint passes through to the backend's
|
|
87
|
-
// RequestLogFilterParams model which has many more filter fields than the OpenAPI spec lists.
|
|
88
|
-
// We include the full set from the backend for completeness. The OpenAPI spec only documents
|
|
89
|
-
// a simplified subset (dataset_group_id, variables_to_parse, start_time, end_time, plus the
|
|
90
|
-
// fields we added: prompt_id, prompt_version_id, prompt_label_id, workspace_id, tags, metadata).
|
|
91
|
-
'EXTRA FIELD: POST /api/public/v2/dataset-versions/from-filter-params > "id" not in OpenAPI [tool: create-dataset-version-from-filter-params]',
|
|
92
|
-
'EXTRA FIELD: POST /api/public/v2/dataset-versions/from-filter-params > "limit" not in OpenAPI [tool: create-dataset-version-from-filter-params]',
|
|
93
|
-
'EXTRA FIELD: POST /api/public/v2/dataset-versions/from-filter-params > "tags_and" not in OpenAPI [tool: create-dataset-version-from-filter-params]',
|
|
94
|
-
'EXTRA FIELD: POST /api/public/v2/dataset-versions/from-filter-params > "tags_or" not in OpenAPI [tool: create-dataset-version-from-filter-params]',
|
|
95
|
-
'EXTRA FIELD: POST /api/public/v2/dataset-versions/from-filter-params > "metadata_and" not in OpenAPI [tool: create-dataset-version-from-filter-params]',
|
|
96
|
-
'EXTRA FIELD: POST /api/public/v2/dataset-versions/from-filter-params > "metadata_or" not in OpenAPI [tool: create-dataset-version-from-filter-params]',
|
|
97
|
-
'EXTRA FIELD: POST /api/public/v2/dataset-versions/from-filter-params > "prompt_templates_include" not in OpenAPI [tool: create-dataset-version-from-filter-params]',
|
|
98
|
-
'EXTRA FIELD: POST /api/public/v2/dataset-versions/from-filter-params > "prompt_templates_exclude" not in OpenAPI [tool: create-dataset-version-from-filter-params]',
|
|
99
|
-
'EXTRA FIELD: POST /api/public/v2/dataset-versions/from-filter-params > "starred" not in OpenAPI [tool: create-dataset-version-from-filter-params]',
|
|
100
|
-
'EXTRA FIELD: POST /api/public/v2/dataset-versions/from-filter-params > "status" not in OpenAPI [tool: create-dataset-version-from-filter-params]',
|
|
101
|
-
'EXTRA FIELD: POST /api/public/v2/dataset-versions/from-filter-params > "sort_by" not in OpenAPI [tool: create-dataset-version-from-filter-params]',
|
|
102
|
-
'EXTRA FIELD: POST /api/public/v2/dataset-versions/from-filter-params > "sort_order" not in OpenAPI [tool: create-dataset-version-from-filter-params]',
|
|
103
|
-
'EXTRA FIELD: POST /api/public/v2/dataset-versions/from-filter-params > "order_by_random" not in OpenAPI [tool: create-dataset-version-from-filter-params]',
|
|
104
|
-
|
|
105
|
-
// The OpenAPI spec defines scores as object but backend uses a list of {name, operator, value}.
|
|
106
|
-
// Our array type matches what the backend actually accepts.
|
|
107
|
-
'TYPE MISMATCH: POST /api/public/v2/dataset-versions/from-filter-params > "scores": MCP=array, OpenAPI=object [tool: create-dataset-version-from-filter-params]',
|
|
108
|
-
|
|
109
|
-
// patch-prompt-template-version exists in the backend and OpenAPI but is not yet implemented
|
|
110
|
-
// in MCP. Low priority — publish-prompt-template covers most use cases.
|
|
111
|
-
"MISSING IN MCP: PATCH /rest/prompt-templates/{identifier} (Patch Prompt Template Version)",
|
|
112
|
-
|
|
113
|
-
// OTLP trace ingestion endpoint exists in OpenAPI but is not implemented in MCP.
|
|
114
|
-
// This is a specialized protocol endpoint, not a typical REST API tool.
|
|
115
|
-
"MISSING IN MCP: POST /v1/traces (Ingest Traces (OTLP))",
|
|
116
|
-
|
|
117
|
-
// Semantic search and tags parameters for folder entities are documented in the reference docs
|
|
118
|
-
// (https://docs.promptlayer.com/reference/list-folder-entities) but not yet in the OpenAPI spec.
|
|
119
|
-
'EXTRA FIELD: GET /api/public/v2/folders/entities > "semantic_search" not in OpenAPI [tool: get-folder-entities]',
|
|
120
|
-
'EXTRA FIELD: GET /api/public/v2/folders/entities > "semantic_search_top_k" not in OpenAPI [tool: get-folder-entities]',
|
|
121
|
-
'EXTRA FIELD: GET /api/public/v2/folders/entities > "semantic_search_threshold" not in OpenAPI [tool: get-folder-entities]',
|
|
122
|
-
'EXTRA FIELD: GET /api/public/v2/folders/entities > "tags" not in OpenAPI [tool: get-folder-entities]',
|
|
123
|
-
|
|
124
|
-
// The OpenAPI spec represents tags as anyOf(string, array) but our Zod union serializes
|
|
125
|
-
// as "union" vs "union(string,array)". Functionally equivalent.
|
|
126
|
-
'TYPE MISMATCH: GET /prompt-templates > "tags": MCP=union, OpenAPI=union(string,array) [tool: list-prompt-templates]',
|
|
127
|
-
|
|
128
|
-
// filter_type OpenAPI uses oneOf vs our Zod union. Functionally equivalent.
|
|
129
|
-
'TYPE MISMATCH: GET /api/public/v2/folders/entities > "filter_type": MCP=union, OpenAPI=oneOf [tool: get-folder-entities]',
|
|
130
|
-
|
|
131
|
-
// workspace_id is optional in MCP because the API key implicitly provides it.
|
|
132
|
-
// OpenAPI marks it required but the backend falls back to the API key's workspace.
|
|
133
|
-
'REQUIRED MISMATCH: GET /api/public/v2/folders/entities > "workspace_id": MCP=false, OpenAPI=true [tool: get-folder-entities]',
|
|
134
|
-
'REQUIRED MISMATCH: GET /api/public/v2/folders/resolve-id > "workspace_id": MCP=false, OpenAPI=true [tool: resolve-folder-id]',
|
|
135
|
-
];
|
|
136
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
137
|
-
|
|
138
|
-
type OpenAPIEndpoint = {
|
|
139
|
-
method: string;
|
|
140
|
-
path: string;
|
|
141
|
-
summary: string;
|
|
142
|
-
operationId: string;
|
|
143
|
-
pathParams: { name: string; type: string; required: boolean }[];
|
|
144
|
-
queryParams: { name: string; type: string; required: boolean; enum?: string[] }[];
|
|
145
|
-
bodyFields: { name: string; type: string; required: boolean; enum?: string[] }[];
|
|
146
|
-
};
|
|
147
|
-
|
|
148
|
-
type MCPTool = {
|
|
149
|
-
toolName: string;
|
|
150
|
-
method: string;
|
|
151
|
-
path: string;
|
|
152
|
-
description: string;
|
|
153
|
-
fields: { name: string; type: string; required: boolean; enum?: string[] }[];
|
|
154
|
-
};
|
|
155
|
-
|
|
156
|
-
function endpointKey(method: string, path: string): string {
|
|
157
|
-
return `${method} ${path}`;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
function main() {
|
|
161
|
-
const openapi: OpenAPIEndpoint[] = JSON.parse(readFileSync(OPENAPI_PATH, "utf-8"));
|
|
162
|
-
const mcp: MCPTool[] = JSON.parse(readFileSync(MCP_PATH, "utf-8"));
|
|
163
|
-
|
|
164
|
-
const allIssues: string[] = [];
|
|
165
|
-
|
|
166
|
-
const openapiMap = new Map<string, OpenAPIEndpoint>();
|
|
167
|
-
for (const ep of openapi) openapiMap.set(endpointKey(ep.method, ep.path), ep);
|
|
168
|
-
|
|
169
|
-
const mcpMap = new Map<string, MCPTool>();
|
|
170
|
-
for (const tool of mcp) mcpMap.set(endpointKey(tool.method, tool.path), tool);
|
|
171
|
-
|
|
172
|
-
// 1. Endpoints in OpenAPI but missing from MCP
|
|
173
|
-
for (const [key, ep] of openapiMap) {
|
|
174
|
-
if (!mcpMap.has(key)) {
|
|
175
|
-
allIssues.push(`MISSING IN MCP: ${key} (${ep.summary})`);
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
// 2. Endpoints in MCP but not in OpenAPI
|
|
180
|
-
for (const [key, tool] of mcpMap) {
|
|
181
|
-
if (!openapiMap.has(key)) {
|
|
182
|
-
allIssues.push(`EXTRA IN MCP (not in OpenAPI): ${key} (tool: ${tool.toolName})`);
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
// 3. Field-level comparison for matching endpoints
|
|
187
|
-
for (const [key, tool] of mcpMap) {
|
|
188
|
-
const ep = openapiMap.get(key);
|
|
189
|
-
if (!ep) continue;
|
|
190
|
-
|
|
191
|
-
const openapiFields = new Map<string, { type: string; required: boolean; enum?: string[] }>();
|
|
192
|
-
for (const p of ep.pathParams) openapiFields.set(p.name, p);
|
|
193
|
-
for (const p of ep.queryParams) openapiFields.set(p.name, { type: p.type, required: p.required, ...(p.enum ? { enum: p.enum } : {}) });
|
|
194
|
-
for (const f of ep.bodyFields) openapiFields.set(f.name, { type: f.type, required: f.required, ...(f.enum ? { enum: f.enum } : {}) });
|
|
195
|
-
|
|
196
|
-
const mcpFields = new Map<string, { type: string; required: boolean; enum?: string[] }>();
|
|
197
|
-
for (const f of tool.fields) mcpFields.set(f.name, f);
|
|
198
|
-
|
|
199
|
-
for (const [name, spec] of openapiFields) {
|
|
200
|
-
if (!mcpFields.has(name)) {
|
|
201
|
-
allIssues.push(`MISSING FIELD: ${key} > "${name}" (${spec.type}, ${spec.required ? "required" : "optional"}) [tool: ${tool.toolName}]`);
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
for (const [name] of mcpFields) {
|
|
206
|
-
if (!openapiFields.has(name)) {
|
|
207
|
-
allIssues.push(`EXTRA FIELD: ${key} > "${name}" not in OpenAPI [tool: ${tool.toolName}]`);
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
for (const [name, mcpField] of mcpFields) {
|
|
212
|
-
const oaField = openapiFields.get(name);
|
|
213
|
-
if (!oaField) continue;
|
|
214
|
-
|
|
215
|
-
if (mcpField.required !== oaField.required) {
|
|
216
|
-
allIssues.push(`REQUIRED MISMATCH: ${key} > "${name}": MCP=${mcpField.required}, OpenAPI=${oaField.required} [tool: ${tool.toolName}]`);
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
const mcpType = mcpField.type.replace("number", "integer");
|
|
220
|
-
const oaType = oaField.type.replace("number", "integer");
|
|
221
|
-
if (mcpType !== oaType && oaType !== "unknown") {
|
|
222
|
-
allIssues.push(`TYPE MISMATCH: ${key} > "${name}": MCP=${mcpField.type}, OpenAPI=${oaField.type} [tool: ${tool.toolName}]`);
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
// Partition into known vs new
|
|
228
|
-
const knownSet = new Set(KNOWN_EXCEPTIONS);
|
|
229
|
-
const knownIssues = allIssues.filter((i) => knownSet.has(i));
|
|
230
|
-
const newIssues = allIssues.filter((i) => !knownSet.has(i));
|
|
231
|
-
|
|
232
|
-
// Report
|
|
233
|
-
if (allIssues.length === 0) {
|
|
234
|
-
console.log("PASS: All MCP tools are in sync with the OpenAPI spec. No issues.");
|
|
235
|
-
process.exit(0);
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
if (knownIssues.length > 0) {
|
|
239
|
-
console.log(`Known exceptions (${knownIssues.length}):`);
|
|
240
|
-
knownIssues.forEach((i) => console.log(` [OK] ${i}`));
|
|
241
|
-
console.log();
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
if (newIssues.length === 0) {
|
|
245
|
-
console.log(`PASS: ${knownIssues.length} known exception(s), 0 new issues.`);
|
|
246
|
-
process.exit(0);
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
console.log(`FAIL: ${newIssues.length} new issue(s) found:\n`);
|
|
250
|
-
|
|
251
|
-
const buckets: Record<string, string[]> = {
|
|
252
|
-
"Missing Endpoints": [],
|
|
253
|
-
"Extra Endpoints": [],
|
|
254
|
-
"Missing Fields": [],
|
|
255
|
-
"Extra Fields": [],
|
|
256
|
-
"Type/Required Mismatches": [],
|
|
257
|
-
};
|
|
258
|
-
|
|
259
|
-
for (const i of newIssues) {
|
|
260
|
-
if (i.startsWith("MISSING IN MCP")) buckets["Missing Endpoints"].push(i);
|
|
261
|
-
else if (i.startsWith("EXTRA IN MCP")) buckets["Extra Endpoints"].push(i);
|
|
262
|
-
else if (i.startsWith("MISSING FIELD")) buckets["Missing Fields"].push(i);
|
|
263
|
-
else if (i.startsWith("EXTRA FIELD")) buckets["Extra Fields"].push(i);
|
|
264
|
-
else buckets["Type/Required Mismatches"].push(i);
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
for (const [label, items] of Object.entries(buckets)) {
|
|
268
|
-
if (items.length === 0) continue;
|
|
269
|
-
console.log(`--- ${label} ---`);
|
|
270
|
-
items.forEach((i) => console.log(` ${i}`));
|
|
271
|
-
console.log();
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
process.exit(1);
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
main();
|
|
@@ -1,122 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Extracts the MCP tool definitions from our types.ts and outputs a canonical
|
|
3
|
-
* JSON file matching the shape of openapi-endpoints.json for comparison.
|
|
4
|
-
*
|
|
5
|
-
* Usage: npx tsx scripts/extract-mcp-tools.ts
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import { TOOL_DEFINITIONS } from "../src/types.js";
|
|
9
|
-
import { zodToJsonSchema } from "zod-to-json-schema";
|
|
10
|
-
|
|
11
|
-
const OUTPUT_PATH = new URL("./mcp-tools.json", import.meta.url).pathname;
|
|
12
|
-
|
|
13
|
-
// Map from MCP tool name to the REST endpoint it wraps.
|
|
14
|
-
// This is the manual mapping — it's the one thing we maintain by hand.
|
|
15
|
-
// The diff script checks everything else automatically.
|
|
16
|
-
// Fields renamed in MCP vs OpenAPI (MCP name → OpenAPI name)
|
|
17
|
-
const FIELD_ALIASES: Record<string, Record<string, string>> = {
|
|
18
|
-
"get-prompt-template": { "prompt_name": "identifier" },
|
|
19
|
-
};
|
|
20
|
-
|
|
21
|
-
const TOOL_TO_ENDPOINT: Record<string, { method: string; path: string }> = {
|
|
22
|
-
"get-prompt-template": { method: "POST", path: "/prompt-templates/{identifier}" },
|
|
23
|
-
"get-prompt-template-raw": { method: "GET", path: "/prompt-templates/{identifier}" },
|
|
24
|
-
"list-prompt-templates": { method: "GET", path: "/prompt-templates" },
|
|
25
|
-
"publish-prompt-template": { method: "POST", path: "/rest/prompt-templates" },
|
|
26
|
-
"list-prompt-template-labels":{ method: "GET", path: "/prompt-templates/{identifier}/labels" },
|
|
27
|
-
"create-prompt-label": { method: "POST", path: "/prompts/{prompt_id}/label" },
|
|
28
|
-
"move-prompt-label": { method: "PATCH", path: "/prompt-labels/{prompt_label_id}" },
|
|
29
|
-
"delete-prompt-label": { method: "DELETE", path: "/prompt-labels/{prompt_label_id}" },
|
|
30
|
-
"get-snippet-usage": { method: "GET", path: "/prompt-templates/{identifier}/snippet-usage" },
|
|
31
|
-
"search-request-logs": { method: "POST", path: "/api/public/v2/requests/search" },
|
|
32
|
-
"get-request": { method: "GET", path: "/api/public/v2/requests/{request_id}" },
|
|
33
|
-
"log-request": { method: "POST", path: "/log-request" },
|
|
34
|
-
"create-spans-bulk": { method: "POST", path: "/spans-bulk" },
|
|
35
|
-
"list-datasets": { method: "GET", path: "/api/public/v2/datasets" },
|
|
36
|
-
"get-dataset-rows": { method: "GET", path: "/api/public/v2/datasets/{dataset_id}/rows" },
|
|
37
|
-
"create-dataset-group": { method: "POST", path: "/api/public/v2/dataset-groups" },
|
|
38
|
-
"create-dataset-version-from-file": { method: "POST", path: "/api/public/v2/dataset-versions/from-file" },
|
|
39
|
-
"create-dataset-version-from-filter-params": { method: "POST", path: "/api/public/v2/dataset-versions/from-filter-params" },
|
|
40
|
-
"list-evaluations": { method: "GET", path: "/api/public/v2/evaluations" },
|
|
41
|
-
"get-evaluation-rows": { method: "GET", path: "/api/public/v2/evaluations/{evaluation_id}/rows" },
|
|
42
|
-
"create-report": { method: "POST", path: "/reports" },
|
|
43
|
-
"run-report": { method: "POST", path: "/reports/{report_id}/run" },
|
|
44
|
-
"get-report": { method: "GET", path: "/reports/{report_id}" },
|
|
45
|
-
"get-report-score": { method: "GET", path: "/reports/{report_id}/score" },
|
|
46
|
-
"update-report-score-card": { method: "PATCH", path: "/reports/{report_id}/score-card" },
|
|
47
|
-
"delete-reports-by-name": { method: "DELETE", path: "/reports/name/{report_name}" },
|
|
48
|
-
"list-workflows": { method: "GET", path: "/workflows" },
|
|
49
|
-
"create-workflow": { method: "POST", path: "/rest/workflows" },
|
|
50
|
-
"patch-workflow": { method: "PATCH", path: "/rest/workflows/{workflow_id_or_name}" },
|
|
51
|
-
"run-workflow": { method: "POST", path: "/workflows/{workflow_name}/run" },
|
|
52
|
-
"get-workflow-version-execution-results": { method: "GET", path: "/workflow-version-execution-results" },
|
|
53
|
-
"get-workflow": { method: "GET", path: "/workflows/{workflow_id_or_name}" },
|
|
54
|
-
"create-folder": { method: "POST", path: "/api/public/v2/folders" },
|
|
55
|
-
"edit-folder": { method: "PATCH", path: "/api/public/v2/folders/{folder_id}" },
|
|
56
|
-
"get-folder-entities": { method: "GET", path: "/api/public/v2/folders/entities" },
|
|
57
|
-
"move-folder-entities": { method: "POST", path: "/api/public/v2/folders/entities" },
|
|
58
|
-
"delete-folder-entities": { method: "DELETE", path: "/api/public/v2/folders/entities" },
|
|
59
|
-
"resolve-folder-id": { method: "GET", path: "/api/public/v2/folders/resolve-id" },
|
|
60
|
-
};
|
|
61
|
-
|
|
62
|
-
type ToolEntry = {
|
|
63
|
-
toolName: string;
|
|
64
|
-
method: string;
|
|
65
|
-
path: string;
|
|
66
|
-
description: string;
|
|
67
|
-
fields: { name: string; type: string; required: boolean; enum?: string[] }[];
|
|
68
|
-
};
|
|
69
|
-
|
|
70
|
-
function normalizeType(schema: Record<string, unknown>): string {
|
|
71
|
-
const t = schema.type;
|
|
72
|
-
if (typeof t === "string") return t;
|
|
73
|
-
if (Array.isArray(t)) {
|
|
74
|
-
const nonNull = t.filter((x: unknown) => x !== "null");
|
|
75
|
-
return nonNull.length === 1 ? String(nonNull[0]) : `union(${nonNull.join(",")})`;
|
|
76
|
-
}
|
|
77
|
-
if (schema.anyOf) return "union";
|
|
78
|
-
if (schema.oneOf) return "oneOf";
|
|
79
|
-
return "unknown";
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
async function main() {
|
|
83
|
-
const tools: ToolEntry[] = [];
|
|
84
|
-
|
|
85
|
-
for (const [name, def] of Object.entries(TOOL_DEFINITIONS)) {
|
|
86
|
-
const mapping = TOOL_TO_ENDPOINT[name];
|
|
87
|
-
if (!mapping) {
|
|
88
|
-
console.error(`WARNING: No endpoint mapping for tool "${name}"`);
|
|
89
|
-
continue;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
const jsonSchema = zodToJsonSchema(def.inputSchema, { target: "openApi3" }) as Record<string, unknown>;
|
|
93
|
-
const properties = (jsonSchema.properties || {}) as Record<string, Record<string, unknown>>;
|
|
94
|
-
const required = new Set((jsonSchema.required || []) as string[]);
|
|
95
|
-
|
|
96
|
-
const aliases = FIELD_ALIASES[name] || {};
|
|
97
|
-
const fields: ToolEntry["fields"] = [];
|
|
98
|
-
for (const [fieldName, fieldSchema] of Object.entries(properties)) {
|
|
99
|
-
if (fieldName === "api_key") continue; // skip MCP-only field
|
|
100
|
-
fields.push({
|
|
101
|
-
name: aliases[fieldName] || fieldName,
|
|
102
|
-
type: normalizeType(fieldSchema),
|
|
103
|
-
required: required.has(fieldName),
|
|
104
|
-
...(fieldSchema.enum ? { enum: fieldSchema.enum as string[] } : {}),
|
|
105
|
-
});
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
tools.push({
|
|
109
|
-
toolName: name,
|
|
110
|
-
method: mapping.method,
|
|
111
|
-
path: mapping.path,
|
|
112
|
-
description: def.description,
|
|
113
|
-
fields,
|
|
114
|
-
});
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
const fs = await import("fs");
|
|
118
|
-
fs.writeFileSync(OUTPUT_PATH, JSON.stringify(tools, null, 2) + "\n");
|
|
119
|
-
console.error(`Wrote ${tools.length} tools to ${OUTPUT_PATH}`);
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
main().catch((e) => { console.error(e); process.exit(1); });
|
|
@@ -1,150 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Fetches the PromptLayer OpenAPI spec from GitHub and extracts a canonical
|
|
3
|
-
* list of endpoints with their parameters. Writes to scripts/openapi-endpoints.json.
|
|
4
|
-
*
|
|
5
|
-
* Usage: npx tsx scripts/fetch-openapi-endpoints.ts
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
const OPENAPI_URL = "https://raw.githubusercontent.com/magnivorg/prompt-layer-docs/master/openapi.json";
|
|
9
|
-
const OUTPUT_PATH = new URL("./openapi-endpoints.json", import.meta.url).pathname;
|
|
10
|
-
|
|
11
|
-
type OpenAPIParam = {
|
|
12
|
-
name: string;
|
|
13
|
-
in: string;
|
|
14
|
-
required?: boolean;
|
|
15
|
-
schema?: { type?: string; enum?: string[]; default?: unknown; [k: string]: unknown };
|
|
16
|
-
[k: string]: unknown;
|
|
17
|
-
};
|
|
18
|
-
|
|
19
|
-
type OpenAPISchema = {
|
|
20
|
-
type?: string;
|
|
21
|
-
properties?: Record<string, { type?: string | unknown[]; enum?: string[]; [k: string]: unknown }>;
|
|
22
|
-
required?: string[];
|
|
23
|
-
$ref?: string;
|
|
24
|
-
[k: string]: unknown;
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
type CanonicalEndpoint = {
|
|
28
|
-
method: string;
|
|
29
|
-
path: string;
|
|
30
|
-
summary: string;
|
|
31
|
-
operationId: string;
|
|
32
|
-
pathParams: { name: string; type: string; required: boolean }[];
|
|
33
|
-
queryParams: { name: string; type: string; required: boolean; enum?: string[]; default?: unknown }[];
|
|
34
|
-
bodyFields: { name: string; type: string; required: boolean; enum?: string[] }[];
|
|
35
|
-
};
|
|
36
|
-
|
|
37
|
-
function resolveRef(ref: string, spec: Record<string, unknown>): Record<string, unknown> {
|
|
38
|
-
const parts = ref.replace("#/", "").split("/");
|
|
39
|
-
let obj: unknown = spec;
|
|
40
|
-
for (const p of parts) obj = (obj as Record<string, unknown>)[p];
|
|
41
|
-
return obj as Record<string, unknown>;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
function resolveSchema(schema: OpenAPISchema, spec: Record<string, unknown>): OpenAPISchema {
|
|
45
|
-
if (schema.$ref) return resolveRef(schema.$ref, spec) as OpenAPISchema;
|
|
46
|
-
return schema;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
function normalizeType(schema: { type?: string | unknown[]; anyOf?: unknown[]; oneOf?: unknown[]; [k: string]: unknown }): string {
|
|
50
|
-
if (typeof schema.type === "string") return schema.type;
|
|
51
|
-
if (Array.isArray(schema.type)) {
|
|
52
|
-
const nonNull = schema.type.filter((t) => t !== "null");
|
|
53
|
-
return nonNull.length === 1 ? String(nonNull[0]) : String(schema.type);
|
|
54
|
-
}
|
|
55
|
-
if (schema.anyOf) {
|
|
56
|
-
const types = (schema.anyOf as Array<{ type?: string }>).map((s) => s.type).filter((t) => t && t !== "null");
|
|
57
|
-
return types.length === 1 ? types[0]! : `union(${types.join(",")})`;
|
|
58
|
-
}
|
|
59
|
-
if (schema.oneOf) return "oneOf";
|
|
60
|
-
return "unknown";
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
async function main() {
|
|
64
|
-
console.error(`Fetching OpenAPI spec from ${OPENAPI_URL}...`);
|
|
65
|
-
const resp = await fetch(OPENAPI_URL);
|
|
66
|
-
if (!resp.ok) throw new Error(`HTTP ${resp.status}: ${resp.statusText}`);
|
|
67
|
-
const spec = (await resp.json()) as Record<string, unknown>;
|
|
68
|
-
const paths = spec.paths as Record<string, Record<string, Record<string, unknown>>>;
|
|
69
|
-
|
|
70
|
-
const endpoints: CanonicalEndpoint[] = [];
|
|
71
|
-
|
|
72
|
-
for (const [path, methods] of Object.entries(paths).sort(([a], [b]) => a.localeCompare(b))) {
|
|
73
|
-
for (const method of ["get", "post", "put", "patch", "delete"]) {
|
|
74
|
-
const op = methods[method];
|
|
75
|
-
if (!op) continue;
|
|
76
|
-
|
|
77
|
-
const endpoint: CanonicalEndpoint = {
|
|
78
|
-
method: method.toUpperCase(),
|
|
79
|
-
path,
|
|
80
|
-
summary: (op.summary as string) || "",
|
|
81
|
-
operationId: (op.operationId as string) || "",
|
|
82
|
-
pathParams: [],
|
|
83
|
-
queryParams: [],
|
|
84
|
-
bodyFields: [],
|
|
85
|
-
};
|
|
86
|
-
|
|
87
|
-
// Parameters (path + query)
|
|
88
|
-
const params = (op.parameters || []) as OpenAPIParam[];
|
|
89
|
-
for (const p of params) {
|
|
90
|
-
const resolved = p.$ref ? (resolveRef(p.$ref as string, spec) as OpenAPIParam) : p;
|
|
91
|
-
if (resolved.in === "header") continue; // skip X-API-KEY
|
|
92
|
-
const schema = resolved.schema || {};
|
|
93
|
-
const entry = {
|
|
94
|
-
name: resolved.name,
|
|
95
|
-
type: normalizeType(schema),
|
|
96
|
-
required: resolved.required ?? false,
|
|
97
|
-
...(schema.enum ? { enum: schema.enum } : {}),
|
|
98
|
-
...(schema.default !== undefined ? { default: schema.default } : {}),
|
|
99
|
-
};
|
|
100
|
-
if (resolved.in === "path") endpoint.pathParams.push(entry);
|
|
101
|
-
else if (resolved.in === "query") endpoint.queryParams.push(entry);
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
// Request body
|
|
105
|
-
const reqBody = op.requestBody as { content?: Record<string, { schema?: OpenAPISchema }> } | undefined;
|
|
106
|
-
if (reqBody?.content) {
|
|
107
|
-
for (const ct of Object.values(reqBody.content)) {
|
|
108
|
-
let schema = ct.schema;
|
|
109
|
-
if (!schema) continue;
|
|
110
|
-
schema = resolveSchema(schema, spec);
|
|
111
|
-
|
|
112
|
-
// Handle anyOf/oneOf wrappers (e.g. anyOf: [$ref, null])
|
|
113
|
-
if (!schema.properties && (schema.anyOf || schema.oneOf)) {
|
|
114
|
-
const variants = (schema.anyOf || schema.oneOf) as OpenAPISchema[];
|
|
115
|
-
const nonNull = variants.filter((v) => v.type !== "null");
|
|
116
|
-
if (nonNull.length === 1) {
|
|
117
|
-
schema = resolveSchema(nonNull[0], spec);
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
if (schema.type === "object" && schema.properties) {
|
|
122
|
-
const required = new Set(schema.required || []);
|
|
123
|
-
for (const [name, propSchema] of Object.entries(schema.properties)) {
|
|
124
|
-
const resolved = propSchema.$ref
|
|
125
|
-
? (resolveRef(propSchema.$ref as string, spec) as typeof propSchema)
|
|
126
|
-
: propSchema;
|
|
127
|
-
endpoint.bodyFields.push({
|
|
128
|
-
name,
|
|
129
|
-
type: normalizeType(resolved),
|
|
130
|
-
required: required.has(name),
|
|
131
|
-
...(resolved.enum ? { enum: resolved.enum as string[] } : {}),
|
|
132
|
-
});
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
endpoints.push(endpoint);
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
const { writeFileSync } = await import("fs");
|
|
143
|
-
writeFileSync(OUTPUT_PATH, JSON.stringify(endpoints, null, 2) + "\n");
|
|
144
|
-
console.error(`Wrote ${endpoints.length} endpoints to ${OUTPUT_PATH}`);
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
main().catch((e) => {
|
|
148
|
-
console.error(e);
|
|
149
|
-
process.exit(1);
|
|
150
|
-
});
|