@promptlayer/mcp-server 1.9.0 → 1.11.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/src/client.ts CHANGED
@@ -112,4 +112,17 @@ export class PromptLayerClient {
112
112
  moveFolderEntities(body: Body) { return this.post("/api/public/v2/folders/entities", body); }
113
113
  deleteFolderEntities(body: Body) { return this.request("/api/public/v2/folders/entities", { method: "DELETE", body: JSON.stringify(body) }); }
114
114
  resolveFolderId(params: Body) { return this.get("/api/public/v2/folders/resolve-id", params); }
115
+
116
+ // Skill Collections
117
+ listSkillCollections() { return this.get("/api/public/v2/skill-collections"); }
118
+ createSkillCollection(body: Body) { return this.post("/api/public/v2/skill-collections", body); }
119
+ getSkillCollection(identifier: string, params?: Body) { return this.get(`/api/public/v2/skill-collections/${this.enc(identifier)}`, params); }
120
+ updateSkillCollection(identifier: string, body: Body) { return this.patch(`/api/public/v2/skill-collections/${this.enc(identifier)}`, body); }
121
+ saveSkillCollectionVersion(identifier: string, body: Body) { return this.post(`/api/public/v2/skill-collections/${this.enc(identifier)}/versions`, body); }
122
+
123
+ // Analytics
124
+ getRequestAnalytics(body: Body) { return this.post("/api/public/v2/requests/analytics", body); }
125
+
126
+ // Prompt template patch
127
+ patchPromptTemplateVersion(identifier: string, body: Body) { return this.patch(`/rest/prompt-templates/${this.enc(identifier)}`, body); }
115
128
  }
package/src/handlers.ts CHANGED
@@ -164,4 +164,28 @@ export function registerAllTools(server: any) {
164
164
  (r) => { const c_ = (r as { moved_count?: number }).moved_count; return `Deleted ${c_ ?? 0} entity/entities`; });
165
165
  reg(t["resolve-folder-id"], (c, a) => c.resolveFolderId(body(a)),
166
166
  (r) => { const id = (r as { id?: number }).id; return id ? `Folder ID: ${id}` : "Folder not found"; });
167
+
168
+ // Skill Collections
169
+ reg(t["list-skill-collections"], (c) => c.listSkillCollections(),
170
+ (r) => { const cs = (r as { skill_collections?: unknown[] }).skill_collections; return `${cs?.length ?? 0} skill collection(s)`; });
171
+ reg(t["create-skill-collection"], (c, a) => c.createSkillCollection(body(a)),
172
+ (r) => { const s = (r as { skill_collection?: { name?: string; id?: string } }).skill_collection; return s ? `Skill collection "${s.name}" created (ID: ${s.id})` : "Skill collection created"; });
173
+ reg(t["get-skill-collection"],
174
+ (c, a) => { const { api_key: _, identifier, ...p } = a as { identifier: string; api_key?: string } & Args; return c.getSkillCollection(identifier, p); },
175
+ (r) => { const s = (r as { skill_collection?: { name?: string }; version?: { version_number?: number } }); const name = s.skill_collection?.name ?? ""; const v = s.version?.version_number; return v !== undefined ? `Skill collection "${name}" v${v} retrieved` : `Skill collection "${name}" retrieved`; });
176
+ reg(t["update-skill-collection"],
177
+ (c, a) => { const { api_key: _, identifier, ...b } = a as { identifier: string; api_key?: string } & Args; return c.updateSkillCollection(identifier, b); },
178
+ () => "Skill collection updated");
179
+ reg(t["save-skill-collection-version"],
180
+ (c, a) => { const { api_key: _, identifier, ...b } = a as { identifier: string; api_key?: string } & Args; return c.saveSkillCollectionVersion(identifier, b); },
181
+ (r) => { const v = (r as { version?: { version_number?: number } }).version?.version_number; return v !== undefined ? `Version ${v} saved` : "Version saved"; });
182
+
183
+ // Analytics
184
+ reg(t["get-request-analytics"], (c, a) => c.getRequestAnalytics(body(a)),
185
+ (r) => { const x = r as { totalRequests?: number; totalCost?: number }; const reqs = x.totalRequests ?? 0; const cost = x.totalCost; return cost !== undefined ? `${reqs} request(s), $${cost} total cost` : `${reqs} request(s) analyzed`; });
186
+
187
+ // Prompt template patch
188
+ reg(t["patch-prompt-template-version"],
189
+ (c, a) => { const { api_key: _, identifier, ...b } = a as { identifier: string; api_key?: string } & Args; return c.patchPromptTemplateVersion(identifier, b); },
190
+ (r) => { const v = (r as { version_number?: number }).version_number; return v !== undefined ? `Patched — new version ${v} created` : "Patched — new version created"; });
167
191
  }
package/src/types.ts CHANGED
@@ -83,7 +83,12 @@ export const PublishPromptTemplateArgsSchema = z.object({
83
83
  prompt_name: z.string().describe("Name of the prompt template"),
84
84
  tags: z.array(z.string()).optional().describe("Tags to associate"),
85
85
  folder_id: z.number().int().optional().describe("Folder ID to publish into"),
86
- }).describe("Template metadata: prompt_name (required), tags, folder_id"),
86
+ is_snippet: z.boolean().optional().describe(
87
+ "Mark this template as a reusable snippet (referenced via @@@name@@@ in other prompts). " +
88
+ "Only takes effect when the prompt is first created — publishing a new version of an " +
89
+ "existing template will not flip this flag."
90
+ ),
91
+ }).describe("Template metadata: prompt_name (required), tags, folder_id, is_snippet"),
87
92
  prompt_version: z.object({
88
93
  prompt_template: z.record(z.unknown()).describe("The template content in chat ({type:'chat', messages:[...]}) or completion format"),
89
94
  commit_message: z.string().optional().describe("Commit message (max 72 chars)"),
@@ -218,44 +223,59 @@ export const CreateDatasetVersionFromFileArgsSchema = z.object({
218
223
  api_key: z.string().optional().describe("PromptLayer API key (optional, defaults to PROMPTLAYER_API_KEY env var)"),
219
224
  });
220
225
 
226
+ // ── Structured filter primitives (shared by request search + dataset-from-history) ──
227
+ // NOTE: value/filters use loose types (z.unknown()) because the backend validates
228
+ // operator-field compatibility at runtime. This is tracked as a known exception in
229
+ // scripts/diff-endpoints.ts.
230
+
231
+ const StructuredFilterSchema = z.object({
232
+ field: z.enum([
233
+ "pl_id", "prompt_id", "engine", "provider_type", "input_text", "output_text",
234
+ "prompt_version_number", "input_tokens", "output_tokens", "cost", "latency_ms",
235
+ "request_start_time", "request_end_time", "status",
236
+ "is_json", "is_tool_call", "is_plain_text", "has_trace",
237
+ "tags", "metadata_keys", "metadata", "tool_names",
238
+ "output", "output_keys", "input_variables", "input_variable_keys",
239
+ "turn_count", "tool_call_count",
240
+ ]).describe("Request log field to filter on"),
241
+ operator: z.enum([
242
+ "is", "is_not", "in", "not_in",
243
+ "contains", "not_contains", "starts_with", "ends_with",
244
+ "eq", "neq", "gt", "gte", "lt", "lte", "between",
245
+ "before", "after",
246
+ "is_true", "is_false", "is_empty", "is_not_empty",
247
+ "is_null", "is_not_null",
248
+ "key_equals", "key_not_equals", "key_contains",
249
+ ]).describe("Filter operator (availability depends on field type)"),
250
+ value: z.unknown().optional().describe("Filter value (type depends on operator)"),
251
+ nested_key: z.string().optional().describe("Key name for nested field operators (metadata, output, input_variables)"),
252
+ });
253
+
254
+ const StructuredFilterGroupSchema: z.ZodType = z.object({
255
+ logic: z.enum(["AND", "OR"]).optional().describe("Logical operator (default: AND)"),
256
+ filters: z.array(z.union([StructuredFilterSchema, z.lazy(() => StructuredFilterGroupSchema)])).describe("Filters or nested filter groups"),
257
+ });
258
+
221
259
  // ── Create Dataset Version from Filter Params (POST /api/public/v2/dataset-versions/from-filter-params)
222
260
 
223
261
  export const CreateDatasetVersionFromFilterParamsArgsSchema = z.object({
224
- dataset_group_id: z.number().int().describe("Dataset group ID"),
225
- variables_to_parse: z.array(z.string()).optional().describe("Variables to extract from request logs"),
226
- tags: z.array(z.string()).optional().describe("Filter by tags (simple tag filter)"),
227
- metadata: z.record(z.string()).optional().describe("Simple metadata key-value filter"),
228
- start_time: z.string().optional().describe("Start time filter (ISO 8601)"),
229
- end_time: z.string().optional().describe("End time filter (ISO 8601)"),
230
- id: z.number().int().optional().describe("Filter by specific request log ID"),
231
- limit: z.number().int().optional().describe("Limit number of request logs to pull"),
232
- tags_and: z.array(z.string()).optional().describe("Filter by tags (AND logic all must match)"),
233
- tags_or: z.array(z.string()).optional().describe("Filter by tags (OR logic — any can match)"),
234
- metadata_and: z.array(z.object({ key: z.string(), value: z.string() })).optional().describe("Metadata filters with AND logic (all must match). Each item: {key, value}"),
235
- metadata_or: z.array(z.object({ key: z.string(), value: z.string() })).optional().describe("Metadata filters with OR logic (any can match). Each item: {key, value}"),
236
- scores: z.array(z.object({
237
- name: z.string().describe("Score name"),
238
- operator: z.enum([">", "<", ">=", "<=", "="]).describe("Comparison operator"),
239
- value: z.number().int().describe("Score value to compare against"),
240
- })).optional().describe("Filter by score criteria. Each item: {name, operator, value}"),
241
- prompt_templates_include: z.array(z.object({
242
- name: z.string().describe("Prompt template name"),
243
- version_numbers: z.array(z.number().int()).optional().describe("Filter to specific version numbers"),
244
- labels: z.array(z.string()).optional().describe("Filter to specific labels"),
245
- })).optional().describe("Include request logs matching these prompt templates. This is the primary way to filter by prompt — use the prompt name (not ID)."),
246
- prompt_templates_exclude: z.array(z.object({
247
- name: z.string().describe("Prompt template name"),
248
- version_numbers: z.array(z.number().int()).optional().describe("Filter to specific version numbers"),
249
- labels: z.array(z.string()).optional().describe("Filter to specific labels"),
250
- })).optional().describe("Exclude request logs matching these prompt templates"),
251
- starred: z.boolean().optional().describe("Filter by starred status"),
252
- status: z.array(z.enum(["SUCCESS", "WARNING", "ERROR"])).optional().describe("Filter by request log status"),
262
+ dataset_group_id: z.number().int().describe("Dataset group ID to create the new version under"),
263
+ request_log_ids: z.array(z.number().int().positive()).optional().describe(
264
+ "Static snapshot mode: pin the dataset to an explicit list of request log IDs (capped at 50,000). " +
265
+ "≤50 IDs run synchronously; >50 are processed asynchronously. " +
266
+ "Datasets created this way are not refreshable."
267
+ ),
268
+ filter_group: StructuredFilterGroupSchema.optional().describe(
269
+ "Structured filter mode: same shape as search-request-logs (AND/OR groups of field/operator/value filters). " +
270
+ "Always processed asynchronously. Persisted on the dataset so refresh_dataset can replay it."
271
+ ),
272
+ q: z.string().optional().describe("Free-text search query applied alongside filter_group"),
253
273
  sort_by: z.enum([
254
- "request_start_time", "input_tokens", "output_tokens", "price",
255
- "score", "latency", "prompt_name", "status",
274
+ "request_start_time", "input_tokens", "output_tokens", "cost",
275
+ "latency_ms", "status", "turn_count", "tool_call_count",
256
276
  ]).optional().describe("Sort field"),
257
- sort_order: z.enum(["asc", "desc"]).optional().describe("Sort direction"),
258
- order_by_random: z.boolean().optional().describe("Random ordering (requires limit)"),
277
+ sort_order: z.enum(["asc", "desc"]).optional().describe("Sort direction (defaults to desc when sort_by is set)"),
278
+ variables_to_parse: z.array(z.string()).optional().describe("Input variable names to extract as dataset columns"),
259
279
  api_key: z.string().optional().describe("PromptLayer API key (optional, defaults to PROMPTLAYER_API_KEY env var)"),
260
280
  });
261
281
 
@@ -597,49 +617,129 @@ export const ResolveFolderIdArgsSchema = z.object({
597
617
  });
598
618
 
599
619
 
600
- // ── Search Request Logs (POST /api/public/v2/requests/search) ────────────
601
- // NOTE: The StructuredFilter and StructuredFilterGroup schemas use loose types
602
- // (z.unknown()) for value/filters because the backend validates operator-field
603
- // compatibility at runtime. This is tracked as a known exception in scripts/diff-endpoints.ts.
620
+ // ── Skill Collections ────────────────────────────────────────────────────
621
+ // Public API endpoints under /api/public/v2/skill-collections.
622
+ // MCP supports JSON bodies only (multipart/form-data with ZIP archive uploads
623
+ // is intentionally not exposed agents should send file contents inline).
604
624
 
605
- const StructuredFilterSchema = z.object({
606
- field: z.enum([
607
- "pl_id", "prompt_id", "engine", "provider_type", "input_text", "output_text",
608
- "prompt_version_number", "input_tokens", "output_tokens", "cost", "latency_ms",
609
- "request_start_time", "request_end_time", "status",
610
- "is_json", "is_tool_call", "is_plain_text",
611
- "tags", "metadata_keys", "metadata", "tool_names",
612
- "output", "output_keys", "input_variables", "input_variable_keys",
613
- ]).describe("Request log field to filter on"),
614
- operator: z.enum([
615
- "is", "is_not", "in", "not_in",
616
- "contains", "not_contains", "starts_with", "ends_with",
617
- "eq", "neq", "gt", "gte", "lt", "lte", "between",
618
- "before", "after",
619
- "is_true", "is_false", "is_empty", "is_not_empty",
620
- "is_null", "is_not_null",
621
- "key_equals", "key_not_equals", "key_contains",
622
- ]).describe("Filter operator (availability depends on field type)"),
623
- value: z.unknown().optional().describe("Filter value (type depends on operator)"),
624
- nested_key: z.string().optional().describe("Key name for nested field operators (metadata, output, input_variables)"),
625
+ export const ListSkillCollectionsArgsSchema = z.object({
626
+ api_key: z.string().optional().describe("PromptLayer API key (optional, defaults to PROMPTLAYER_API_KEY env var)"),
625
627
  });
626
628
 
627
- const StructuredFilterGroupSchema: z.ZodType = z.object({
628
- logic: z.enum(["AND", "OR"]).optional().describe("Logical operator (default: AND)"),
629
- filters: z.array(z.union([StructuredFilterSchema, z.lazy(() => StructuredFilterGroupSchema)])).describe("Filters or nested filter groups"),
629
+ const SkillCollectionFileSchema = z.object({
630
+ path: z.string().describe("Relative file path inside the collection (e.g. 'README.md', 'src/util.ts')"),
631
+ content: z.string().optional().describe("File contents as a string. Defaults to empty string if omitted."),
630
632
  });
631
633
 
632
- export const SearchRequestLogsArgsSchema = z.object({
634
+ export const CreateSkillCollectionArgsSchema = z.object({
635
+ name: z.string().describe("Collection name. Must be a valid root folder name (will be made unique within the workspace)."),
636
+ description: z.string().optional().describe("Optional human-readable description"),
637
+ folder_id: z.number().int().optional().describe("Folder ID to place the collection into"),
638
+ provider: z.string().optional().describe("Provider hint (e.g. 'claude', 'cursor'). Auto-detected from file paths if omitted."),
639
+ files: z.array(SkillCollectionFileSchema).optional().describe("Initial files for the collection. Each item is {path, content}."),
640
+ commit_message: z.string().optional().describe("Commit message for the initial version"),
641
+ api_key: z.string().optional().describe("PromptLayer API key (optional, defaults to PROMPTLAYER_API_KEY env var)"),
642
+ });
643
+
644
+ export const GetSkillCollectionArgsSchema = z.object({
645
+ identifier: z.string().describe("Skill collection UUID, name, or root_path"),
646
+ label: z.string().optional().describe("Release label to pin the version (mutually exclusive with version)"),
647
+ version: z.number().int().min(1).optional().describe("Version number to pin (mutually exclusive with label)"),
648
+ api_key: z.string().optional().describe("PromptLayer API key (optional, defaults to PROMPTLAYER_API_KEY env var)"),
649
+ });
650
+
651
+ export const UpdateSkillCollectionArgsSchema = z.object({
652
+ identifier: z.string().describe("Skill collection UUID, name, or root_path"),
653
+ name: z.string().optional().describe("New collection name (will be made unique within the workspace if it collides)"),
654
+ description: z.string().optional().describe("New description. Pass empty string to clear."),
655
+ api_key: z.string().optional().describe("PromptLayer API key (optional, defaults to PROMPTLAYER_API_KEY env var)"),
656
+ });
657
+
658
+ const SkillCollectionFileMoveSchema = z.object({
659
+ old_path: z.string().describe("Existing relative path"),
660
+ new_path: z.string().describe("New relative path"),
661
+ });
662
+
663
+ export const SaveSkillCollectionVersionArgsSchema = z.object({
664
+ identifier: z.string().describe("Skill collection UUID, name, or root_path"),
665
+ file_updates: z.array(SkillCollectionFileSchema).optional().describe("Files to add or overwrite. Each item is {path, content}. Files not mentioned are carried forward from the previous version."),
666
+ moves: z.array(SkillCollectionFileMoveSchema).optional().describe("Files to rename: [{old_path, new_path}, ...]"),
667
+ deletes: z.array(z.string()).optional().describe("Relative paths of files to delete from the new version"),
668
+ commit_message: z.string().optional().describe("Commit message describing what changed"),
669
+ release_label: z.string().optional().describe("Release label to attach to the new version (e.g. 'production')"),
670
+ api_key: z.string().optional().describe("PromptLayer API key (optional, defaults to PROMPTLAYER_API_KEY env var)"),
671
+ });
672
+
673
+
674
+ // ── Patch Prompt Template Version (PATCH /rest/prompt-templates/{identifier}) ──
675
+ // Partial update: fetches the base version, applies field-level patches, creates a new version.
676
+
677
+ export const PatchPromptTemplateVersionArgsSchema = z.object({
678
+ identifier: z.string().describe("Prompt template name or numeric ID"),
679
+ version: z.number().int().optional().describe("Base version number to patch from (mutually exclusive with label; defaults to latest)"),
680
+ label: z.string().optional().describe("Release label identifying the base version (mutually exclusive with version)"),
681
+ messages: z.union([z.record(z.unknown()), z.array(z.record(z.unknown()))]).optional().describe(
682
+ "Chat templates only. " +
683
+ "Object form: index-based patch ({\"0\": {...}}) — only listed indices are updated, others preserved. " +
684
+ "Array form: full replacement of all messages."
685
+ ),
686
+ tools: z.union([z.record(z.unknown()), z.array(z.record(z.unknown())), z.null()]).optional().describe(
687
+ "Chat templates only. Same patching behavior as messages. Pass null to remove all tools."
688
+ ),
689
+ functions: z.union([z.record(z.unknown()), z.array(z.record(z.unknown())), z.null()]).optional().describe(
690
+ "Chat templates only. Same patching behavior as messages. Pass null to remove all functions."
691
+ ),
692
+ function_call: z.union([z.string(), z.record(z.unknown()), z.null()]).optional().describe("Replaces the function_call setting. Null removes."),
693
+ tool_choice: z.union([z.string(), z.record(z.unknown()), z.null()]).optional().describe("Replaces the tool_choice setting. Null removes."),
694
+ content: z.union([z.record(z.unknown()), z.array(z.record(z.unknown()))]).optional().describe(
695
+ "Completion templates only. Same index-based / full-replacement behavior as messages."
696
+ ),
697
+ model_parameters: z.record(z.unknown()).optional().describe("Shallow-merged into existing model parameters (e.g. temperature, max_tokens)."),
698
+ response_format: z.union([z.record(z.unknown()), z.null()]).optional().describe("Convenience field to set response_format inside model parameters. Null removes. Cannot be combined with response_format inside model_parameters."),
699
+ commit_message: z.string().optional().describe("Commit message for the new version"),
700
+ release_labels: z.array(z.string()).optional().describe("Release labels to create or move to the new version"),
701
+ api_key: z.string().optional().describe("PromptLayer API key (optional, defaults to PROMPTLAYER_API_KEY env var)"),
702
+ });
703
+
704
+
705
+
706
+
707
+ // ── Request log query (shared by /requests/search and /requests/analytics) ──
708
+ // Backend models: search uses PostStructuredSearchRequest = RequestLogQuery + page/per_page/include_prompt_name;
709
+ // analytics uses RequestLogQuery directly. We share the 4 RequestLogQuery fields here.
710
+
711
+ const RequestLogQueryShape = {
633
712
  filter_group: StructuredFilterGroupSchema.optional().describe("Filter group with AND/OR logic, supports nesting. Wrap multiple filters in an AND group."),
634
713
  q: z.string().optional().describe("Free-text search across prompt input and LLM output"),
714
+ sort_by: z.enum([
715
+ "request_start_time", "input_tokens", "output_tokens", "cost", "latency_ms", "status",
716
+ "turn_count", "tool_call_count",
717
+ ]).optional().describe("Sort field"),
718
+ sort_order: z.enum(["asc", "desc"]).optional().describe("Sort direction (must be provided with sort_by)"),
719
+ };
720
+
721
+ // ── Search Request Logs (POST /api/public/v2/requests/search) ────────────
722
+ // (StructuredFilter / StructuredFilterGroup are defined higher up so the
723
+ // dataset-from-filter-params endpoint can reuse them.)
724
+
725
+ export const SearchRequestLogsArgsSchema = z.object({
726
+ ...RequestLogQueryShape,
635
727
  page: z.number().int().optional().describe("Page number (default: 1)"),
636
728
  per_page: z.number().int().optional().describe("Items per page (max: 25)"),
637
- sort_by: z.enum(["request_start_time", "input_tokens", "output_tokens", "cost", "latency_ms", "status"]).optional().describe("Sort field"),
638
- sort_order: z.enum(["asc", "desc"]).optional().describe("Sort direction (must be provided with sort_by)"),
639
729
  include_prompt_name: z.boolean().optional().describe("Include prompt template name in results"),
640
730
  api_key: z.string().optional().describe("PromptLayer API key (optional, defaults to PROMPTLAYER_API_KEY env var)"),
641
731
  });
642
732
 
733
+ // ── Get Request Analytics (POST /api/public/v2/requests/analytics) ───────
734
+ // Same RequestLogQuery body as search-request-logs but returns aggregated
735
+ // charts (requests/tokens/cost over time, latency stats, model & prompt
736
+ // breakdowns) instead of paginated rows.
737
+
738
+ export const GetRequestAnalyticsArgsSchema = z.object({
739
+ ...RequestLogQueryShape,
740
+ api_key: z.string().optional().describe("PromptLayer API key (optional, defaults to PROMPTLAYER_API_KEY env var)"),
741
+ });
742
+
643
743
  // ── Get Request (GET /api/public/v2/requests/{request_id}) ───────────────
644
744
 
645
745
  export const GetRequestArgsSchema = z.object({
@@ -715,11 +815,14 @@ export const TOOL_DEFINITIONS = {
715
815
  name: "publish-prompt-template",
716
816
  description:
717
817
  "Create a new version of a prompt template. " +
718
- "Body has two required objects: prompt_template (with prompt_name, tags, folder_id) and " +
818
+ "Body has two required objects: prompt_template (with prompt_name, tags, folder_id, is_snippet) and " +
719
819
  "prompt_version (with prompt_template content in chat/completion format, commit_message, metadata). " +
720
820
  "Optionally assign release_labels. " +
721
821
  "IMPORTANT: If the prompt uses snippets, preserve @@@snippet_name@@@ markers in the content. " +
722
- "Do not inline snippet text — this breaks snippet references.",
822
+ "Do not inline snippet text — this breaks snippet references. " +
823
+ "To create a snippet (a reusable fragment referenced by other prompts), set " +
824
+ "prompt_template.is_snippet=true on the first publish. The flag is only honored on initial " +
825
+ "creation; later versions cannot flip an existing template into a snippet or vice versa.",
723
826
  inputSchema: PublishPromptTemplateArgsSchema,
724
827
  annotations: { readOnlyHint: false },
725
828
  },
@@ -764,9 +867,9 @@ export const TOOL_DEFINITIONS = {
764
867
  "Operators by field type:\n" +
765
868
  " - String fields (engine, provider_type): is, is_not, in, not_in\n" +
766
869
  " - Text fields (input_text, output_text): contains, not_contains, starts_with, ends_with\n" +
767
- " - Numeric fields (cost, latency_ms, input_tokens, output_tokens): eq, neq, gt, gte, lt, lte, between (value=[min,max]), is_null, is_not_null\n" +
870
+ " - Numeric fields (cost, latency_ms, input_tokens, output_tokens, turn_count, tool_call_count): eq, neq, gt, gte, lt, lte, between (value=[min,max]), is_null, is_not_null\n" +
768
871
  " - Datetime fields (request_start_time, request_end_time): is, before, after, between (value=[start,end] as ISO 8601)\n" +
769
- " - Boolean fields (is_json, is_tool_call, is_plain_text): is_true, is_false\n" +
872
+ " - Boolean fields (is_json, is_tool_call, is_plain_text, has_trace): is_true, is_false\n" +
770
873
  " - Array fields (tags, metadata_keys, tool_names, output_keys, input_variable_keys): contains, not_contains, in, not_in, is_empty, is_not_empty\n" +
771
874
  " - Nested fields (metadata, output, input_variables): key_equals, key_not_equals, key_contains, in, not_in, is_empty, is_not_empty — requires nested_key\n\n" +
772
875
  "EXAMPLES:\n" +
@@ -849,7 +952,12 @@ export const TOOL_DEFINITIONS = {
849
952
  },
850
953
  "create-dataset-version-from-filter-params": {
851
954
  name: "create-dataset-version-from-filter-params",
852
- description: "Create a dataset version from request log history using filter criteria. Populated asynchronously.",
955
+ description:
956
+ "Create a dataset version from request log history. Two modes:\n" +
957
+ " 1. request_log_ids — pin to an explicit list of IDs (≤50 sync, >50 async). Snapshot, not refreshable.\n" +
958
+ " 2. filter_group (+ optional q) — structured AND/OR filter, same shape as search-request-logs. " +
959
+ "Async; persisted on the dataset so refresh_dataset can replay it.\n" +
960
+ "Use variables_to_parse to extract specific input variables as dataset columns.",
853
961
  inputSchema: CreateDatasetVersionFromFilterParamsArgsSchema,
854
962
  annotations: { readOnlyHint: false },
855
963
  },
@@ -1065,4 +1173,74 @@ export const TOOL_DEFINITIONS = {
1065
1173
  inputSchema: ResolveFolderIdArgsSchema,
1066
1174
  annotations: { readOnlyHint: true },
1067
1175
  },
1176
+
1177
+ // ── Skill Collections ───────────────────────────────────────────────
1178
+ "list-skill-collections": {
1179
+ name: "list-skill-collections",
1180
+ description: "List all skill collections in the workspace. Returns each collection's UUID, name, root_path, provider, description, and timestamps.",
1181
+ inputSchema: ListSkillCollectionsArgsSchema,
1182
+ annotations: { readOnlyHint: true },
1183
+ },
1184
+ "create-skill-collection": {
1185
+ name: "create-skill-collection",
1186
+ description:
1187
+ "Create a new skill collection with optional initial files. Each file is {path, content} where path is relative inside the collection " +
1188
+ "(e.g. 'README.md' or 'src/util.ts'). Provider is auto-detected from file paths if omitted (e.g. .claude/* → 'claude'). " +
1189
+ "Names are made unique within the workspace if they collide.",
1190
+ inputSchema: CreateSkillCollectionArgsSchema,
1191
+ annotations: { readOnlyHint: false },
1192
+ },
1193
+ "get-skill-collection": {
1194
+ name: "get-skill-collection",
1195
+ description:
1196
+ "Fetch a skill collection by UUID, name, or root_path. Returns the collection metadata, the version object, and a 'files' map of " +
1197
+ "{relative_path: file_content}. Pin a specific version with 'version' (number) or 'label' (release label) — these are mutually exclusive. " +
1198
+ "If neither is provided, returns the latest committed version.",
1199
+ inputSchema: GetSkillCollectionArgsSchema,
1200
+ annotations: { readOnlyHint: true },
1201
+ },
1202
+ "update-skill-collection": {
1203
+ name: "update-skill-collection",
1204
+ description: "Rename a skill collection or update its description. To save new file contents as a version, use save-skill-collection-version instead.",
1205
+ inputSchema: UpdateSkillCollectionArgsSchema,
1206
+ annotations: { readOnlyHint: false },
1207
+ },
1208
+ "save-skill-collection-version": {
1209
+ name: "save-skill-collection-version",
1210
+ description:
1211
+ "Save a new version of a skill collection. Apply file changes via three lists: " +
1212
+ "file_updates (add or overwrite — files not mentioned are carried forward unchanged), " +
1213
+ "moves (rename), and deletes (remove). Optionally attach a release_label to the new version.",
1214
+ inputSchema: SaveSkillCollectionVersionArgsSchema,
1215
+ annotations: { readOnlyHint: false },
1216
+ },
1217
+
1218
+ // ── Analytics ───────────────────────────────────────────────────────
1219
+ "get-request-analytics": {
1220
+ name: "get-request-analytics",
1221
+ description:
1222
+ "Aggregate analytics across request logs — totals (requests, tokens, cost), time-series breakdowns, latency percentiles, " +
1223
+ "and most-used models/prompts. Body is the same shape as search-request-logs (filter_group, q, sort_by, sort_order); the response is aggregated, not paginated rows. " +
1224
+ "Use this to answer questions like 'how much have we spent on GPT-4o this week?' or 'what's the p90 latency for prod traffic?'.",
1225
+ inputSchema: GetRequestAnalyticsArgsSchema,
1226
+ annotations: { readOnlyHint: true },
1227
+ },
1228
+
1229
+ // ── Prompt Template Patch ───────────────────────────────────────────
1230
+ "patch-prompt-template-version": {
1231
+ name: "patch-prompt-template-version",
1232
+ description:
1233
+ "Partially update a prompt template by creating a new version with merged changes. Fetches the base version (latest by default, or pin via version/label), " +
1234
+ "applies field-level patches, and creates a new version.\n\n" +
1235
+ "MERGE BEHAVIOR:\n" +
1236
+ " - messages/tools/functions/content: object form is index-based patch ({\"0\": {...}} updates only listed indices); array form fully replaces.\n" +
1237
+ " - tools/functions/function_call/tool_choice/response_format: pass null to remove.\n" +
1238
+ " - model_parameters: shallow merge — existing keys are preserved unless overwritten.\n" +
1239
+ " - release_labels: creates or moves the labels to the new version.\n\n" +
1240
+ "Use this for surgical edits (e.g. tweak the system message, add a tool, change temperature) without resending the whole template. " +
1241
+ "For a full rewrite, use publish-prompt-template instead.",
1242
+ inputSchema: PatchPromptTemplateVersionArgsSchema,
1243
+ annotations: { readOnlyHint: false },
1244
+ },
1245
+
1068
1246
  } as const;