@2kw/ai-mcp-server 6.1.0-dev.8 → 6.2.0-dev.13

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.
@@ -0,0 +1,382 @@
1
+ import { z } from "zod";
2
+ import { readFile } from "node:fs/promises";
3
+ import { basename } from "node:path";
4
+ import { BackboneApiError, formatErrorForMcp } from "../errors.js";
5
+ import { getMimeType } from "../mime.js";
6
+ const ChunkingStrategy = z.enum(["AUTO", "FIXED", "HIERARCHICAL", "CUSTOM"]);
7
+ const DocumentStatus = z.enum(["PENDING", "PARSING", "CHUNKING", "EMBEDDING", "READY", "ERROR"]);
8
+ /**
9
+ * Fields shared by knowledge-base create and update — the backend treats
10
+ * every one of these as mandatory on both requests (name, slug,
11
+ * embeddingProviderId, embeddingModel, embeddingDim), so there is no partial
12
+ * update: an update omitting one of them would be rejected by validation.
13
+ */
14
+ const knowledgeBaseFields = {
15
+ name: z.string().min(1).describe("Knowledge base name"),
16
+ slug: z.string().min(1).describe("URL-safe slug, lowercase letters and digits joined by single dashes"),
17
+ embeddingProviderId: z.string().min(1).describe("Provider id serving the embedding model"),
18
+ embeddingModel: z.string().min(1).describe("Embedding model id"),
19
+ embeddingDim: z.number().int().positive().describe("Embedding dimension. Fixed at creation — on update this must match the existing value, it is not applied as a change."),
20
+ description: z.string().optional().describe("Knowledge base description"),
21
+ chunkingStrategy: ChunkingStrategy.optional().describe("Chunking strategy"),
22
+ chunkSize: z.number().int().min(50).max(4000).optional().describe("Chunk size (50-4000)"),
23
+ chunkOverlap: z.number().int().min(0).max(2000).optional().describe("Chunk overlap (0-2000), must be smaller than chunkSize"),
24
+ parentChunkSize: z.number().int().min(100).max(8000).optional().describe("Parent chunk size for hierarchical chunking (100-8000), must exceed chunkSize"),
25
+ hybridSearchEnabled: z.boolean().optional().describe("Enable hybrid dense + lexical search"),
26
+ rerankerProviderId: z.string().optional().describe("Provider id used for reranking"),
27
+ };
28
+ function buildKnowledgeBaseBody(params) {
29
+ const body = {
30
+ name: params.name,
31
+ slug: params.slug,
32
+ embeddingProviderId: params.embeddingProviderId,
33
+ embeddingModel: params.embeddingModel,
34
+ embeddingDim: params.embeddingDim,
35
+ };
36
+ for (const key of [
37
+ "description",
38
+ "chunkingStrategy",
39
+ "chunkSize",
40
+ "chunkOverlap",
41
+ "parentChunkSize",
42
+ "hybridSearchEnabled",
43
+ "rerankerProviderId",
44
+ ]) {
45
+ if (params[key] !== undefined)
46
+ body[key] = params[key];
47
+ }
48
+ return body;
49
+ }
50
+ /**
51
+ * Format a hybrid search response, surfacing each hit's chunkId prominently
52
+ * so an agent can chain a hit straight into 2kw_resolve_citation.
53
+ */
54
+ function formatSearchResponse(data) {
55
+ const result = data;
56
+ const hits = result.hits ?? [];
57
+ const lines = hits.map((h, i) => {
58
+ const pages = h.pageStart !== undefined ? ` pages ${h.pageStart}-${h.pageEnd ?? h.pageStart}` : "";
59
+ const heading = h.headingPath?.length ? ` [${h.headingPath.join(" > ")}]` : "";
60
+ return `${i + 1}. chunkId=${h.chunkId} score=${h.score?.toFixed(4)} document="${h.documentName}" (id: ${h.documentId})${pages}${heading}\n ${h.content}`;
61
+ });
62
+ const header = `Search results for "${result.query}"${result.degraded ? ` (degraded: ${result.degraded})` : ""} — ${hits.length} hit(s). Use 2kw_resolve_citation with a chunkId to re-fetch a passage.`;
63
+ return `${header}\n${lines.join("\n") || "(no hits)"}`;
64
+ }
65
+ export function register(server, client) {
66
+ // ── list_knowledge_bases ─────────────────────────────────────────────────
67
+ server.tool("2kw_list_knowledge_bases", "List the organization's knowledge bases as a page, newest first by default.", {
68
+ page: z.number().optional().default(0).describe("Page number (0-based)"),
69
+ size: z.number().optional().default(20).describe("Page size"),
70
+ }, async ({ page, size }) => {
71
+ try {
72
+ const { data } = await client.GET("/v1/knowledge-bases", {
73
+ params: { query: { pageable: { page, size } } },
74
+ });
75
+ const result = data;
76
+ const lines = (result.content ?? []).map((kb) => `- ${kb.name} (id: ${kb.id}, slug: ${kb.slug}, status: ${kb.status})${kb.description ? ` — ${kb.description}` : ""}`);
77
+ return {
78
+ content: [
79
+ {
80
+ type: "text",
81
+ text: `Knowledge bases (${result.totalElements} total, page ${(result.number ?? 0) + 1}/${result.totalPages}):\n${lines.join("\n") || "(none)"}`,
82
+ },
83
+ ],
84
+ };
85
+ }
86
+ catch (error) {
87
+ return {
88
+ content: [{ type: "text", text: formatErrorForMcp(error) }],
89
+ isError: true,
90
+ };
91
+ }
92
+ });
93
+ // ── create_knowledge_base ────────────────────────────────────────────────
94
+ server.tool("2kw_create_knowledge_base", "Create a knowledge base and its first configuration version. The embedding dimension is fixed at creation.", knowledgeBaseFields, async (params) => {
95
+ try {
96
+ const { data } = await client.POST("/v1/knowledge-bases", {
97
+ body: buildKnowledgeBaseBody(params),
98
+ });
99
+ return {
100
+ content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
101
+ };
102
+ }
103
+ catch (error) {
104
+ return {
105
+ content: [{ type: "text", text: formatErrorForMcp(error) }],
106
+ isError: true,
107
+ };
108
+ }
109
+ });
110
+ // ── get_knowledge_base ───────────────────────────────────────────────────
111
+ server.tool("2kw_get_knowledge_base", "Retrieve a single knowledge base by id.", { knowledgeBaseId: z.string().describe("The knowledge base ID") }, async ({ knowledgeBaseId }) => {
112
+ try {
113
+ const { data } = await client.GET("/v1/knowledge-bases/{id}", {
114
+ params: { path: { id: knowledgeBaseId } },
115
+ });
116
+ return {
117
+ content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
118
+ };
119
+ }
120
+ catch (error) {
121
+ return {
122
+ content: [{ type: "text", text: formatErrorForMcp(error) }],
123
+ isError: true,
124
+ };
125
+ }
126
+ });
127
+ // ── update_knowledge_base ────────────────────────────────────────────────
128
+ server.tool("2kw_update_knowledge_base", "Apply the request's fields to a knowledge base and snapshot the result as the next configuration version. name, slug, embeddingProviderId, embeddingModel and embeddingDim are required on every update; embeddingDim cannot actually change — it must match the existing value or the update is rejected.", { knowledgeBaseId: z.string().describe("The knowledge base ID"), ...knowledgeBaseFields }, async ({ knowledgeBaseId, ...params }) => {
129
+ try {
130
+ const { data } = await client.PATCH("/v1/knowledge-bases/{id}", {
131
+ params: { path: { id: knowledgeBaseId } },
132
+ body: buildKnowledgeBaseBody(params),
133
+ });
134
+ return {
135
+ content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
136
+ };
137
+ }
138
+ catch (error) {
139
+ return {
140
+ content: [{ type: "text", text: formatErrorForMcp(error) }],
141
+ isError: true,
142
+ };
143
+ }
144
+ });
145
+ // ── delete_knowledge_base ────────────────────────────────────────────────
146
+ server.tool("2kw_delete_knowledge_base", "Soft-delete a knowledge base, releasing its slug. Version history is retained. Requires admin role.", { knowledgeBaseId: z.string().describe("The knowledge base ID to delete") }, async ({ knowledgeBaseId }) => {
147
+ try {
148
+ await client.DELETE("/v1/knowledge-bases/{id}", {
149
+ params: { path: { id: knowledgeBaseId } },
150
+ });
151
+ return {
152
+ content: [{ type: "text", text: `Knowledge base ${knowledgeBaseId} deleted successfully.` }],
153
+ };
154
+ }
155
+ catch (error) {
156
+ return {
157
+ content: [{ type: "text", text: formatErrorForMcp(error) }],
158
+ isError: true,
159
+ };
160
+ }
161
+ });
162
+ // ── list_knowledge_documents ──────────────────────────────────────────────
163
+ server.tool("2kw_list_knowledge_documents", "List a knowledge base's documents as a page, newest first by default. The optional status filter matches on the document's current revision.", {
164
+ knowledgeBaseId: z.string().describe("The knowledge base ID"),
165
+ status: DocumentStatus.optional().describe("Filter by the document's current revision status"),
166
+ page: z.number().optional().default(0).describe("Page number (0-based)"),
167
+ size: z.number().optional().default(20).describe("Page size"),
168
+ }, async ({ knowledgeBaseId, status, page, size }) => {
169
+ try {
170
+ const { data } = await client.GET("/v1/knowledge-bases/{knowledgeBaseId}/documents", {
171
+ params: {
172
+ path: { knowledgeBaseId },
173
+ query: { status, pageable: { page, size } },
174
+ },
175
+ });
176
+ const result = data;
177
+ const lines = (result.content ?? []).map((d) => `- ${d.name} (id: ${d.id}, currentVersion: ${d.currentVersion?.status ?? "—"})`);
178
+ return {
179
+ content: [
180
+ {
181
+ type: "text",
182
+ text: `Documents (${result.totalElements} total, page ${(result.number ?? 0) + 1}/${result.totalPages}):\n${lines.join("\n") || "(none)"}`,
183
+ },
184
+ ],
185
+ };
186
+ }
187
+ catch (error) {
188
+ return {
189
+ content: [{ type: "text", text: formatErrorForMcp(error) }],
190
+ isError: true,
191
+ };
192
+ }
193
+ });
194
+ // ── upload_knowledge_documents ────────────────────────────────────────────
195
+ server.tool("2kw_upload_knowledge_documents", "Upload one or more local files into a knowledge base for asynchronous ingestion. Always accepted (202); poll each entry's version with 2kw_get_knowledge_document_version. Files are accepted independently, so one rejected file does not discard the others.", {
196
+ knowledgeBaseId: z.string().describe("The knowledge base ID"),
197
+ filePaths: z.array(z.string()).min(1).describe("Local paths of the files to upload"),
198
+ }, async ({ knowledgeBaseId, filePaths }) => {
199
+ try {
200
+ const formData = new FormData();
201
+ for (const filePath of filePaths) {
202
+ const buffer = await readFile(filePath);
203
+ const filename = basename(filePath);
204
+ formData.append("files", new Blob([buffer], { type: getMimeType(filename) }), filename);
205
+ }
206
+ const { baseUrl, apiKey } = client._config;
207
+ const res = await fetch(`${baseUrl}/v1/knowledge-bases/${encodeURIComponent(knowledgeBaseId)}/documents`, {
208
+ method: "POST",
209
+ headers: { Authorization: `Bearer ${apiKey}` },
210
+ body: formData,
211
+ });
212
+ if (!res.ok) {
213
+ let body;
214
+ try {
215
+ body = await res.json();
216
+ }
217
+ catch {
218
+ body = {
219
+ error: res.statusText,
220
+ message: `HTTP ${res.status}: ${res.statusText}`,
221
+ status: res.status,
222
+ timestamp: new Date().toISOString(),
223
+ };
224
+ }
225
+ throw new BackboneApiError(body);
226
+ }
227
+ const data = (await res.json());
228
+ const lines = data.map((r) => r.error
229
+ ? `- ${r.filename}: rejected — ${r.error}`
230
+ : `- ${r.filename}: accepted, documentId=${r.upload?.document?.id}, versionId=${r.upload?.acceptedVersion?.id}, status=${r.upload?.acceptedVersion?.status}`);
231
+ return {
232
+ content: [{ type: "text", text: `Upload accepted:\n${lines.join("\n")}` }],
233
+ };
234
+ }
235
+ catch (error) {
236
+ return {
237
+ content: [{ type: "text", text: formatErrorForMcp(error) }],
238
+ isError: true,
239
+ };
240
+ }
241
+ });
242
+ // ── attach_knowledge_document_file ───────────────────────────────────────
243
+ server.tool("2kw_attach_knowledge_document_file", "Ingest a file uploaded earlier with 2kw_upload_file (purpose=knowledge) into a knowledge base. Always accepted (202); poll acceptedVersion with 2kw_get_knowledge_document_version. 400 for a file whose purpose is not knowledge, 404 for an unknown file, 410 for a deleted one.", {
244
+ knowledgeBaseId: z.string().describe("The knowledge base ID"),
245
+ fileId: z.string().describe("Id of a file uploaded with purpose=knowledge through 2kw_upload_file"),
246
+ }, async ({ knowledgeBaseId, fileId }) => {
247
+ try {
248
+ const { data } = await client.POST("/v1/knowledge-bases/{knowledgeBaseId}/documents/attach", {
249
+ params: { path: { knowledgeBaseId } },
250
+ body: { fileId },
251
+ });
252
+ return {
253
+ content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
254
+ };
255
+ }
256
+ catch (error) {
257
+ return {
258
+ content: [{ type: "text", text: formatErrorForMcp(error) }],
259
+ isError: true,
260
+ };
261
+ }
262
+ });
263
+ // ── get_knowledge_document ────────────────────────────────────────────────
264
+ server.tool("2kw_get_knowledge_document", "Retrieve a knowledge base document together with the revision retrieval is serving.", {
265
+ knowledgeBaseId: z.string().describe("The knowledge base ID"),
266
+ documentId: z.string().describe("The document ID"),
267
+ }, async ({ knowledgeBaseId, documentId }) => {
268
+ try {
269
+ const { data } = await client.GET("/v1/knowledge-bases/{knowledgeBaseId}/documents/{documentId}", { params: { path: { knowledgeBaseId, documentId } } });
270
+ return {
271
+ content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
272
+ };
273
+ }
274
+ catch (error) {
275
+ return {
276
+ content: [{ type: "text", text: formatErrorForMcp(error) }],
277
+ isError: true,
278
+ };
279
+ }
280
+ });
281
+ // ── delete_knowledge_document ─────────────────────────────────────────────
282
+ server.tool("2kw_delete_knowledge_document", "Soft-delete a document and take every revision out of retrieval. Previously issued citations still resolve. Requires admin role.", {
283
+ knowledgeBaseId: z.string().describe("The knowledge base ID"),
284
+ documentId: z.string().describe("The document ID to delete"),
285
+ }, async ({ knowledgeBaseId, documentId }) => {
286
+ try {
287
+ await client.DELETE("/v1/knowledge-bases/{knowledgeBaseId}/documents/{documentId}", {
288
+ params: { path: { knowledgeBaseId, documentId } },
289
+ });
290
+ return {
291
+ content: [{ type: "text", text: `Document ${documentId} deleted successfully.` }],
292
+ };
293
+ }
294
+ catch (error) {
295
+ return {
296
+ content: [{ type: "text", text: formatErrorForMcp(error) }],
297
+ isError: true,
298
+ };
299
+ }
300
+ });
301
+ // ── get_knowledge_document_version ────────────────────────────────────────
302
+ server.tool("2kw_get_knowledge_document_version", "Retrieve one revision of a document by id — the polling endpoint for an accepted upload. Superseded revisions remain readable.", {
303
+ knowledgeBaseId: z.string().describe("The knowledge base ID"),
304
+ documentId: z.string().describe("The document ID"),
305
+ versionId: z.string().describe("The version ID"),
306
+ }, async ({ knowledgeBaseId, documentId, versionId }) => {
307
+ try {
308
+ const { data } = await client.GET("/v1/knowledge-bases/{knowledgeBaseId}/documents/{documentId}/versions/{versionId}", { params: { path: { knowledgeBaseId, documentId, versionId } } });
309
+ return {
310
+ content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
311
+ };
312
+ }
313
+ catch (error) {
314
+ return {
315
+ content: [{ type: "text", text: formatErrorForMcp(error) }],
316
+ isError: true,
317
+ };
318
+ }
319
+ });
320
+ // ── search_knowledge_base ─────────────────────────────────────────────────
321
+ server.tool("2kw_search_knowledge_base", "Hybrid dense + lexical retrieval with Reciprocal Rank Fusion over a knowledge base. topK is clamped to 1..100. Not paginated — raise topK instead of asking for a later page. Each hit carries a chunkId that can be passed to 2kw_resolve_citation to re-fetch the passage later.", {
322
+ knowledgeBaseId: z.string().describe("The knowledge base ID"),
323
+ query: z.string().min(1).describe("The search query"),
324
+ topK: z.number().int().min(1).max(100).optional().describe("Number of results to return (default backend-defined, clamped 1-100)"),
325
+ metadataFilter: z.record(z.string(), z.unknown()).optional().describe("Filter hits by document metadata"),
326
+ }, async ({ knowledgeBaseId, query, topK, metadataFilter }) => {
327
+ try {
328
+ const body = { query };
329
+ if (topK !== undefined)
330
+ body.topK = topK;
331
+ if (metadataFilter !== undefined)
332
+ body.metadataFilter = metadataFilter;
333
+ const { data } = await client.POST("/v1/knowledge-bases/{knowledgeBaseId}/search", {
334
+ params: { path: { knowledgeBaseId } },
335
+ body: body,
336
+ });
337
+ return { content: [{ type: "text", text: formatSearchResponse(data) }] };
338
+ }
339
+ catch (error) {
340
+ return {
341
+ content: [{ type: "text", text: formatErrorForMcp(error) }],
342
+ isError: true,
343
+ };
344
+ }
345
+ });
346
+ // ── resolve_citation ──────────────────────────────────────────────────────
347
+ server.tool("2kw_resolve_citation", "Resolve a citation's chunkId (from 2kw_search_knowledge_base or a response citation) to the passage it points at, exactly as it was indexed. Deliberately resolves passages from superseded revisions — flagged with isCurrentVersion=false. If the document was deleted the citation still resolves with ids/pages/headings intact but the passage withheld (textStatus=WITHDRAWN).", {
348
+ knowledgeBaseId: z.string().describe("The knowledge base ID"),
349
+ chunkId: z.string().describe("The chunk ID from a search hit or response citation"),
350
+ }, async ({ knowledgeBaseId, chunkId }) => {
351
+ try {
352
+ const { data } = await client.GET("/v1/knowledge-bases/{knowledgeBaseId}/citations/{chunkId}", { params: { path: { knowledgeBaseId, chunkId } } });
353
+ return {
354
+ content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
355
+ };
356
+ }
357
+ catch (error) {
358
+ return {
359
+ content: [{ type: "text", text: formatErrorForMcp(error) }],
360
+ isError: true,
361
+ };
362
+ }
363
+ });
364
+ // ── list_provider_embedding_models ────────────────────────────────────────
365
+ server.tool("2kw_list_provider_embedding_models", "List embedding models served by a provider that fit a provisioned embedding dimension. Empty when the provider type has no catalogued embedding models; never an error. Use this to pick embeddingModel/embeddingDim for 2kw_create_knowledge_base.", { providerId: z.string().describe("The provider ID") }, async ({ providerId }) => {
366
+ try {
367
+ const { data } = await client.GET("/v1/providers/{providerId}/embedding-models", {
368
+ params: { path: { providerId } },
369
+ });
370
+ return {
371
+ content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
372
+ };
373
+ }
374
+ catch (error) {
375
+ return {
376
+ content: [{ type: "text", text: formatErrorForMcp(error) }],
377
+ isError: true,
378
+ };
379
+ }
380
+ });
381
+ }
382
+ //# sourceMappingURL=knowledge.js.map
@@ -0,0 +1,5 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import type { ApiClient } from "../client.js";
3
+ /** Register plugin install/sync/detach tools (#640). Writes need an admin key. */
4
+ export declare function register(server: McpServer, client: ApiClient): void;
5
+ //# sourceMappingURL=plugins.d.ts.map
@@ -0,0 +1,98 @@
1
+ import { z } from "zod";
2
+ import { formatErrorForMcp } from "../errors.js";
3
+ // Spring accepts flat pagination parameters despite the generated pageable wrapper.
4
+ function paginationParams(params) {
5
+ return {
6
+ page: params.page ?? 0,
7
+ size: params.size ?? 20,
8
+ sort: params.sort ? [params.sort] : undefined,
9
+ };
10
+ }
11
+ function ok(data) {
12
+ return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
13
+ }
14
+ function failed(error) {
15
+ return { content: [{ type: "text", text: formatErrorForMcp(error) }], isError: true };
16
+ }
17
+ const refPolicy = z
18
+ .string()
19
+ .optional()
20
+ .describe("track:<branch-or-tag> to follow it, pin:<sha> to freeze (default track:main)");
21
+ /** Register plugin install/sync/detach tools (#640). Writes need an admin key. */
22
+ export function register(server, client) {
23
+ server.tool("2kw_list_plugins", "List Claude Code plugins installed in the org, with optional status filter and pagination.", {
24
+ status: z.enum(["ACTIVE", "DISABLED"]).optional(),
25
+ page: z.number().int().min(0).optional(),
26
+ size: z.number().int().min(1).max(100).optional(),
27
+ sort: z.string().optional().describe("Sort field and direction"),
28
+ }, async (params) => {
29
+ try {
30
+ const { data } = await client.GET("/v1/plugins", {
31
+ params: { query: { status: params.status, ...paginationParams(params) } },
32
+ });
33
+ return ok(data);
34
+ }
35
+ catch (error) {
36
+ return failed(error);
37
+ }
38
+ });
39
+ server.tool("2kw_get_plugin", "Get an installed plugin by ID, including its last sync report.", { pluginId: z.string() }, async (params) => {
40
+ try {
41
+ const { data } = await client.GET("/v1/plugins/{id}", { params: { path: { id: params.pluginId } } });
42
+ return ok(data);
43
+ }
44
+ catch (error) {
45
+ return failed(error);
46
+ }
47
+ });
48
+ server.tool("2kw_install_plugin", "Install a plugin from a git repository (https, allow-listed host) and run its first sync: skills are imported with provenance and the sync report is returned.", {
49
+ gitUrl: z.string().url().describe("https remote of a repository in Claude Code plugin layout"),
50
+ name: z.string().optional().describe("Org-local name; default is the manifest name, slugified"),
51
+ refPolicy,
52
+ }, async (params) => {
53
+ try {
54
+ const { data } = await client.POST("/v1/plugins", {
55
+ body: { gitUrl: params.gitUrl, name: params.name, refPolicy: params.refPolicy },
56
+ });
57
+ return ok(data);
58
+ }
59
+ catch (error) {
60
+ return failed(error);
61
+ }
62
+ });
63
+ server.tool("2kw_update_plugin", "Change how an installed plugin follows its repository, or pause it.", {
64
+ pluginId: z.string(),
65
+ refPolicy,
66
+ status: z.enum(["ACTIVE", "DISABLED"]).optional(),
67
+ }, async (params) => {
68
+ try {
69
+ const { data } = await client.PATCH("/v1/plugins/{id}", {
70
+ params: { path: { id: params.pluginId } },
71
+ body: { refPolicy: params.refPolicy, status: params.status },
72
+ });
73
+ return ok(data);
74
+ }
75
+ catch (error) {
76
+ return failed(error);
77
+ }
78
+ });
79
+ server.tool("2kw_sync_plugin", "Sync an installed plugin now: resolves the ref, imports changed skills and moves the plugin label. Unchanged is a no-op.", { pluginId: z.string() }, async (params) => {
80
+ try {
81
+ const { data } = await client.POST("/v1/plugins/{id}/sync", { params: { path: { id: params.pluginId } } });
82
+ return ok(data);
83
+ }
84
+ catch (error) {
85
+ return failed(error);
86
+ }
87
+ });
88
+ server.tool("2kw_delete_plugin", "Detach an installed plugin. Imported skills remain as ordinary skills and stop receiving updates.", { pluginId: z.string() }, async (params) => {
89
+ try {
90
+ await client.DELETE("/v1/plugins/{id}", { params: { path: { id: params.pluginId } } });
91
+ return ok({ detached: params.pluginId });
92
+ }
93
+ catch (error) {
94
+ return failed(error);
95
+ }
96
+ });
97
+ }
98
+ //# sourceMappingURL=plugins.js.map
@@ -1,12 +1,30 @@
1
1
  import { z } from "zod";
2
2
  import { formatErrorForMcp } from "../errors.js";
3
+ const SUBJECT_TYPE = z.enum(["RUN_RESULT", "TRACE", "SPAN", "DATASET_ITEM"]);
3
4
  export function register(server, client) {
4
- server.tool("2kw_record_human_score", "Record a human score for an experiment run result. Upserts by (runResultId, evaluatorId, annotatorId) — a reviewer calling this again replaces their own score, not anyone else's. Score must be between 0.0 and 1.0.", {
5
- runResultId: z.string().describe("Run result id"),
5
+ server.tool("2kw_record_human_score", "Record a human score for a subject — a run result, trace, span, or dataset item. Upserts " +
6
+ "by (subject, evaluatorId, annotatorId), so a reviewer calling this again replaces their " +
7
+ "own score, not anyone else's. Pass either runResultId (legacy shape) or subjectType + " +
8
+ "subjectId; when reviewing an annotation queue item, also pass annotationQueueItemId so " +
9
+ "the server validates the subject matches and can advance the item's status. Score, when " +
10
+ "given, must be between 0.0 and 1.0 — it may be omitted for an open-coding annotation that " +
11
+ "carries only a label or comment.", {
12
+ runResultId: z.string().optional().describe("Run result id (legacy shape)"),
13
+ subjectType: SUBJECT_TYPE.optional().describe("The subject's type — RUN_RESULT, TRACE, SPAN, or DATASET_ITEM"),
14
+ subjectId: z.string().optional().describe("The subject's id"),
15
+ annotationQueueItemId: z
16
+ .string()
17
+ .optional()
18
+ .describe("The annotation queue item this score resolves, if reviewing from a queue"),
6
19
  evaluatorId: z
7
20
  .string()
8
21
  .describe("Evaluator identifier (e.g. 'helpfulness' or a template id)"),
9
- score: z.number().min(0).max(1).describe("Score between 0.0 and 1.0"),
22
+ score: z
23
+ .number()
24
+ .min(0)
25
+ .max(1)
26
+ .optional()
27
+ .describe("Score between 0.0 and 1.0 (optional for open-coding annotations)"),
10
28
  label: z
11
29
  .string()
12
30
  .optional()
@@ -17,6 +35,9 @@ export function register(server, client) {
17
35
  const { data } = await client.POST("/v1/evaluation-scores/human", {
18
36
  body: {
19
37
  runResultId: params.runResultId,
38
+ subjectType: params.subjectType,
39
+ subjectId: params.subjectId,
40
+ annotationQueueItemId: params.annotationQueueItemId,
20
41
  evaluatorId: params.evaluatorId,
21
42
  score: params.score,
22
43
  label: params.label,
@@ -39,5 +60,34 @@ export function register(server, client) {
39
60
  };
40
61
  }
41
62
  });
63
+ // ── list_human_scores ────────────────────────────────────────────────────
64
+ server.tool("2kw_list_human_scores", "List human scores filed against a subject. Defaults to the caller's own scores — pass " +
65
+ "annotatorId to see another reviewer's (or 'me' explicitly).", {
66
+ subjectType: SUBJECT_TYPE.describe("The subject's type"),
67
+ subjectId: z.string().describe("The subject's id"),
68
+ annotatorId: z.string().optional().describe("Annotator id (defaults to the caller)"),
69
+ }, async ({ subjectType, subjectId, annotatorId }) => {
70
+ try {
71
+ const { data } = await client.GET("/v1/evaluation-scores/human", {
72
+ params: { query: { subjectType, subjectId, annotatorId } },
73
+ });
74
+ const scores = (data ?? []);
75
+ const lines = scores.map((s) => `- ${s.evaluatorId}: score=${s.score ?? "—"} label=${s.label ?? "—"} annotator=${s.annotatorId}${s.comment ? ` — ${s.comment}` : ""}`);
76
+ return {
77
+ content: [
78
+ {
79
+ type: "text",
80
+ text: `Human scores for ${subjectType} ${subjectId} (${scores.length}):\n${lines.join("\n") || "(none)"}`,
81
+ },
82
+ ],
83
+ };
84
+ }
85
+ catch (error) {
86
+ return {
87
+ content: [{ type: "text", text: formatErrorForMcp(error) }],
88
+ isError: true,
89
+ };
90
+ }
91
+ });
42
92
  }
43
93
  //# sourceMappingURL=scores.js.map
@@ -0,0 +1,5 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import type { ApiClient } from "../client.js";
3
+ /** Register skill read and import tools (#642). */
4
+ export declare function register(server: McpServer, client: ApiClient): void;
5
+ //# sourceMappingURL=skills.d.ts.map