@2kw/ai-mcp-server 6.1.0-dev.8 → 6.1.1
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/README.md +5 -1
- package/dist/index.js +10 -0
- package/dist/tools/agents.d.ts +4 -0
- package/dist/tools/agents.js +434 -0
- package/dist/tools/annotation-queues.d.ts +4 -0
- package/dist/tools/annotation-queues.js +322 -0
- package/dist/tools/conversations.d.ts +4 -0
- package/dist/tools/conversations.js +102 -0
- package/dist/tools/files.d.ts +4 -0
- package/dist/tools/files.js +143 -0
- package/dist/tools/knowledge.d.ts +4 -0
- package/dist/tools/knowledge.js +382 -0
- package/dist/tools/scores.js +53 -3
- package/package.json +2 -2
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { formatErrorForMcp } from "../errors.js";
|
|
3
|
+
const SUBJECT_TYPE = z.enum(["RUN_RESULT", "TRACE", "SPAN", "DATASET_ITEM"]);
|
|
4
|
+
const ITEM_STATUS = z.enum(["PENDING", "IN_PROGRESS", "DONE", "SKIPPED"]);
|
|
5
|
+
export function register(server, client) {
|
|
6
|
+
// ── list_annotation_queues ──────────────────────────────────────────────
|
|
7
|
+
server.tool("2kw_list_annotation_queues", "List annotation queues in the organization.", {
|
|
8
|
+
archived: z
|
|
9
|
+
.boolean()
|
|
10
|
+
.optional()
|
|
11
|
+
.default(false)
|
|
12
|
+
.describe("List archived queues instead of active ones"),
|
|
13
|
+
}, async ({ archived }) => {
|
|
14
|
+
try {
|
|
15
|
+
const { data } = await client.GET("/v1/annotation-queues", {
|
|
16
|
+
params: { query: { archived } },
|
|
17
|
+
});
|
|
18
|
+
const queues = (data ?? []);
|
|
19
|
+
const lines = queues.map((q) => {
|
|
20
|
+
const counts = q.itemCounts ?? {};
|
|
21
|
+
return `- ${q.name} (id: ${q.id}) — pending=${counts.pending ?? 0} inProgress=${counts.inProgress ?? 0} done=${counts.done ?? 0} skipped=${counts.skipped ?? 0}${q.archivedAt ? ` [archived ${q.archivedAt}]` : ""}`;
|
|
22
|
+
});
|
|
23
|
+
return {
|
|
24
|
+
content: [
|
|
25
|
+
{
|
|
26
|
+
type: "text",
|
|
27
|
+
text: `Annotation queues (${queues.length}):\n${lines.join("\n") || "(none)"}`,
|
|
28
|
+
},
|
|
29
|
+
],
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
catch (error) {
|
|
33
|
+
return {
|
|
34
|
+
content: [{ type: "text", text: formatErrorForMcp(error) }],
|
|
35
|
+
isError: true,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
// ── get_annotation_queue ─────────────────────────────────────────────────
|
|
40
|
+
server.tool("2kw_get_annotation_queue", "Get an annotation queue by its ID, including item counts by status.", {
|
|
41
|
+
queueId: z.string().describe("The annotation queue ID"),
|
|
42
|
+
}, async ({ queueId }) => {
|
|
43
|
+
try {
|
|
44
|
+
const { data } = await client.GET("/v1/annotation-queues/{id}", {
|
|
45
|
+
params: { path: { id: queueId } },
|
|
46
|
+
});
|
|
47
|
+
return {
|
|
48
|
+
content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
return {
|
|
53
|
+
content: [{ type: "text", text: formatErrorForMcp(error) }],
|
|
54
|
+
isError: true,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
// ── create_annotation_queue ──────────────────────────────────────────────
|
|
59
|
+
server.tool("2kw_create_annotation_queue", "Create a new annotation queue to curate subjects for human (or agent-assisted) review.", {
|
|
60
|
+
name: z.string().min(1).max(255).describe("Queue name"),
|
|
61
|
+
description: z.string().max(4000).optional().describe("Queue description"),
|
|
62
|
+
}, async ({ name, description }) => {
|
|
63
|
+
try {
|
|
64
|
+
const { data } = await client.POST("/v1/annotation-queues", {
|
|
65
|
+
body: {
|
|
66
|
+
name,
|
|
67
|
+
...(description !== undefined && { description }),
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
return {
|
|
71
|
+
content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
catch (error) {
|
|
75
|
+
return {
|
|
76
|
+
content: [{ type: "text", text: formatErrorForMcp(error) }],
|
|
77
|
+
isError: true,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
// ── update_annotation_queue ──────────────────────────────────────────────
|
|
82
|
+
server.tool("2kw_update_annotation_queue", "Update an annotation queue's name or description.", {
|
|
83
|
+
queueId: z.string().describe("The annotation queue ID"),
|
|
84
|
+
name: z.string().min(1).max(255).optional().describe("New name"),
|
|
85
|
+
description: z.string().max(4000).optional().describe("New description"),
|
|
86
|
+
}, async ({ queueId, name, description }) => {
|
|
87
|
+
try {
|
|
88
|
+
const body = {};
|
|
89
|
+
if (name !== undefined)
|
|
90
|
+
body.name = name;
|
|
91
|
+
if (description !== undefined)
|
|
92
|
+
body.description = description;
|
|
93
|
+
const { data } = await client.PATCH("/v1/annotation-queues/{id}", {
|
|
94
|
+
params: { path: { id: queueId } },
|
|
95
|
+
body: body,
|
|
96
|
+
});
|
|
97
|
+
return {
|
|
98
|
+
content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
catch (error) {
|
|
102
|
+
return {
|
|
103
|
+
content: [{ type: "text", text: formatErrorForMcp(error) }],
|
|
104
|
+
isError: true,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
// ── archive_annotation_queue ─────────────────────────────────────────────
|
|
109
|
+
server.tool("2kw_archive_annotation_queue", "Archive an annotation queue. Idempotent — archiving an already-archived queue is a no-op.", {
|
|
110
|
+
queueId: z.string().describe("The annotation queue ID to archive"),
|
|
111
|
+
}, async ({ queueId }) => {
|
|
112
|
+
try {
|
|
113
|
+
await client.POST("/v1/annotation-queues/{id}/archive", {
|
|
114
|
+
params: { path: { id: queueId } },
|
|
115
|
+
});
|
|
116
|
+
return {
|
|
117
|
+
content: [{ type: "text", text: `Queue ${queueId} archived.` }],
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
catch (error) {
|
|
121
|
+
return {
|
|
122
|
+
content: [{ type: "text", text: formatErrorForMcp(error) }],
|
|
123
|
+
isError: true,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
// ── find_queue_items_by_subject ──────────────────────────────────────────
|
|
128
|
+
server.tool("2kw_find_queue_items_by_subject", "Find every annotation queue item referencing a given subject, across all queues in the " +
|
|
129
|
+
"organization. Empty when the subject has never been queued. Useful to check whether a " +
|
|
130
|
+
"run result, trace, span, or dataset item is already under review before adding it again.", {
|
|
131
|
+
subjectType: SUBJECT_TYPE.describe("The subject's type"),
|
|
132
|
+
subjectId: z.string().describe("The subject's ID"),
|
|
133
|
+
}, async ({ subjectType, subjectId }) => {
|
|
134
|
+
try {
|
|
135
|
+
const { data } = await client.GET("/v1/annotation-queues/by-subject", {
|
|
136
|
+
params: { query: { subjectType, subjectId } },
|
|
137
|
+
});
|
|
138
|
+
const queueItems = (data ?? []);
|
|
139
|
+
const lines = queueItems.map((i) => `- queue=${i.queueId} item=${i.id} status=${i.status} assignedTo=${i.assignedTo ?? "(unassigned)"}`);
|
|
140
|
+
return {
|
|
141
|
+
content: [
|
|
142
|
+
{
|
|
143
|
+
type: "text",
|
|
144
|
+
text: `Queue items for ${subjectType} ${subjectId} (${queueItems.length}):\n${lines.join("\n") || "(none)"}`,
|
|
145
|
+
},
|
|
146
|
+
],
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
catch (error) {
|
|
150
|
+
return {
|
|
151
|
+
content: [{ type: "text", text: formatErrorForMcp(error) }],
|
|
152
|
+
isError: true,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
// ── list_queue_items ──────────────────────────────────────────────────────
|
|
157
|
+
server.tool("2kw_list_queue_items", "List items in a queue, optionally filtered by status or assignee.", {
|
|
158
|
+
queueId: z.string().describe("The annotation queue ID"),
|
|
159
|
+
status: ITEM_STATUS.optional().describe("Filter by item status"),
|
|
160
|
+
assignedTo: z.string().optional().describe("Filter by assignee user ID"),
|
|
161
|
+
page: z.number().optional().default(0).describe("Page number (0-based)"),
|
|
162
|
+
size: z.number().optional().default(50).describe("Page size"),
|
|
163
|
+
}, async ({ queueId, status, assignedTo, page, size }) => {
|
|
164
|
+
try {
|
|
165
|
+
const { data } = await client.GET("/v1/annotation-queues/{id}/items", {
|
|
166
|
+
params: {
|
|
167
|
+
path: { id: queueId },
|
|
168
|
+
query: { status, assignedTo, page, size },
|
|
169
|
+
},
|
|
170
|
+
});
|
|
171
|
+
const queueItems = (data ?? []);
|
|
172
|
+
const lines = queueItems.map((i) => `- ${i.id} [${i.status}] ${i.subjectType}:${i.subjectId}${i.assignedTo ? ` assignedTo=${i.assignedTo}` : ""}`);
|
|
173
|
+
return {
|
|
174
|
+
content: [
|
|
175
|
+
{
|
|
176
|
+
type: "text",
|
|
177
|
+
text: `Queue ${queueId} items (page ${page ?? 0}, ${queueItems.length} returned):\n${lines.join("\n") || "(none)"}`,
|
|
178
|
+
},
|
|
179
|
+
],
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
catch (error) {
|
|
183
|
+
return {
|
|
184
|
+
content: [{ type: "text", text: formatErrorForMcp(error) }],
|
|
185
|
+
isError: true,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
// ── add_queue_items ───────────────────────────────────────────────────────
|
|
190
|
+
server.tool("2kw_add_queue_items", "Bulk-add subjects to a queue for review. Duplicates (same subject already in the queue) " +
|
|
191
|
+
"are skipped rather than erroring, so this is safe to call repeatedly with an overlapping set.", {
|
|
192
|
+
queueId: z.string().describe("The annotation queue ID"),
|
|
193
|
+
items: z
|
|
194
|
+
.array(z.object({
|
|
195
|
+
subjectType: SUBJECT_TYPE,
|
|
196
|
+
subjectId: z.string().min(1),
|
|
197
|
+
}))
|
|
198
|
+
.min(1)
|
|
199
|
+
.max(1000)
|
|
200
|
+
.describe("Subjects to add, up to 1000 per call"),
|
|
201
|
+
}, async ({ queueId, items }) => {
|
|
202
|
+
try {
|
|
203
|
+
const { data } = await client.POST("/v1/annotation-queues/{id}/items", {
|
|
204
|
+
params: { path: { id: queueId } },
|
|
205
|
+
body: { items },
|
|
206
|
+
});
|
|
207
|
+
const added = (data ?? []);
|
|
208
|
+
return {
|
|
209
|
+
content: [
|
|
210
|
+
{
|
|
211
|
+
type: "text",
|
|
212
|
+
text: `Added ${added.length} item(s) to queue ${queueId} (duplicates skipped).\n\n${JSON.stringify(added, null, 2)}`,
|
|
213
|
+
},
|
|
214
|
+
],
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
catch (error) {
|
|
218
|
+
return {
|
|
219
|
+
content: [{ type: "text", text: formatErrorForMcp(error) }],
|
|
220
|
+
isError: true,
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
// ── claim_next_queue_item ─────────────────────────────────────────────────
|
|
225
|
+
server.tool("2kw_claim_next_queue_item", "Claim the next eligible item in a queue for the caller — the entry point of a review loop. " +
|
|
226
|
+
"Returns a 'no items available' message when the queue has nothing left to claim, which is " +
|
|
227
|
+
"a normal end state, not an error. A typical loop: (1) call this to claim an item and read " +
|
|
228
|
+
"its subjectType/subjectId; (2) inspect that subject with the matching read tool (e.g. an " +
|
|
229
|
+
"experiment run result, a trace, or a dataset item); (3) file the verdict with " +
|
|
230
|
+
"2kw_record_human_score, passing the item's subjectType/subjectId and its id as " +
|
|
231
|
+
"annotationQueueItemId so the server can validate the match; (4) call 2kw_update_queue_item " +
|
|
232
|
+
"to mark the item DONE (or SKIPPED); (5) repeat from step 1 until this tool reports no items " +
|
|
233
|
+
"left.", {
|
|
234
|
+
queueId: z.string().describe("The annotation queue ID to claim from"),
|
|
235
|
+
}, async ({ queueId }) => {
|
|
236
|
+
try {
|
|
237
|
+
const { data, response } = await client.GET("/v1/annotation-queues/{id}/items/next", {
|
|
238
|
+
params: { path: { id: queueId } },
|
|
239
|
+
});
|
|
240
|
+
if (!data || response.status === 204) {
|
|
241
|
+
return {
|
|
242
|
+
content: [
|
|
243
|
+
{
|
|
244
|
+
type: "text",
|
|
245
|
+
text: `No items available to claim in queue ${queueId} — it is empty for you right now.`,
|
|
246
|
+
},
|
|
247
|
+
],
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
const item = data;
|
|
251
|
+
return {
|
|
252
|
+
content: [
|
|
253
|
+
{
|
|
254
|
+
type: "text",
|
|
255
|
+
text: `Claimed item ${item.id} (subject: ${item.subjectType}:${item.subjectId}, status: ${item.status}).\n` +
|
|
256
|
+
`Next: review the subject, file a score with 2kw_record_human_score ` +
|
|
257
|
+
`(annotationQueueItemId="${item.id}"), then call 2kw_update_queue_item to close it out.\n\n` +
|
|
258
|
+
`${JSON.stringify(item, null, 2)}`,
|
|
259
|
+
},
|
|
260
|
+
],
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
catch (error) {
|
|
264
|
+
return {
|
|
265
|
+
content: [{ type: "text", text: formatErrorForMcp(error) }],
|
|
266
|
+
isError: true,
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
});
|
|
270
|
+
// ── get_queue_item ────────────────────────────────────────────────────────
|
|
271
|
+
server.tool("2kw_get_queue_item", "Get a single queue item by its ID.", {
|
|
272
|
+
queueId: z.string().describe("The annotation queue ID"),
|
|
273
|
+
itemId: z.string().describe("The queue item ID"),
|
|
274
|
+
}, async ({ queueId, itemId }) => {
|
|
275
|
+
try {
|
|
276
|
+
const { data } = await client.GET("/v1/annotation-queues/{id}/items/{itemId}", {
|
|
277
|
+
params: { path: { id: queueId, itemId } },
|
|
278
|
+
});
|
|
279
|
+
return {
|
|
280
|
+
content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
catch (error) {
|
|
284
|
+
return {
|
|
285
|
+
content: [{ type: "text", text: formatErrorForMcp(error) }],
|
|
286
|
+
isError: true,
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
});
|
|
290
|
+
// ── update_queue_item ─────────────────────────────────────────────────────
|
|
291
|
+
server.tool("2kw_update_queue_item", "Update a queue item's status or assignee — the closing step of a review loop started by " +
|
|
292
|
+
"2kw_claim_next_queue_item. Set status=DONE once a verdict has been filed (see " +
|
|
293
|
+
"2kw_record_human_score), or SKIPPED to pass on an item without scoring it. Clear " +
|
|
294
|
+
"assignedTo (pass an empty string) to release it back to the pool for another reviewer.", {
|
|
295
|
+
queueId: z.string().describe("The annotation queue ID"),
|
|
296
|
+
itemId: z.string().describe("The queue item ID"),
|
|
297
|
+
status: ITEM_STATUS.optional().describe("New status"),
|
|
298
|
+
assignedTo: z.string().optional().describe("New assignee user ID (empty string to unassign)"),
|
|
299
|
+
}, async ({ queueId, itemId, status, assignedTo }) => {
|
|
300
|
+
try {
|
|
301
|
+
const body = {};
|
|
302
|
+
if (status !== undefined)
|
|
303
|
+
body.status = status;
|
|
304
|
+
if (assignedTo !== undefined)
|
|
305
|
+
body.assignedTo = assignedTo;
|
|
306
|
+
const { data } = await client.PATCH("/v1/annotation-queues/{id}/items/{itemId}", {
|
|
307
|
+
params: { path: { id: queueId, itemId } },
|
|
308
|
+
body: body,
|
|
309
|
+
});
|
|
310
|
+
return {
|
|
311
|
+
content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
catch (error) {
|
|
315
|
+
return {
|
|
316
|
+
content: [{ type: "text", text: formatErrorForMcp(error) }],
|
|
317
|
+
isError: true,
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
//# sourceMappingURL=annotation-queues.js.map
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { formatErrorForMcp } from "../errors.js";
|
|
3
|
+
export function register(server, client) {
|
|
4
|
+
// ── create_conversation ─────────────────────────────────────────────────
|
|
5
|
+
server.tool("2kw_create_conversation", "Create a conversation whose id can be passed as the 'conversation' parameter on the responses API, so an agent turn can be replayed and extended across calls.", {
|
|
6
|
+
items: z.array(z.unknown()).optional().describe("Initial items to seed the conversation with"),
|
|
7
|
+
metadata: z.record(z.string(), z.unknown()).optional().describe("Arbitrary metadata (JSON object)"),
|
|
8
|
+
}, async ({ items, metadata }) => {
|
|
9
|
+
try {
|
|
10
|
+
const body = {};
|
|
11
|
+
if (items !== undefined)
|
|
12
|
+
body.items = items;
|
|
13
|
+
if (metadata !== undefined)
|
|
14
|
+
body.metadata = metadata;
|
|
15
|
+
const { data } = await client.POST("/v1/conversations", {
|
|
16
|
+
body: Object.keys(body).length > 0 ? body : undefined,
|
|
17
|
+
});
|
|
18
|
+
return {
|
|
19
|
+
content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
catch (error) {
|
|
23
|
+
return {
|
|
24
|
+
content: [{ type: "text", text: formatErrorForMcp(error) }],
|
|
25
|
+
isError: true,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
// ── get_conversation ─────────────────────────────────────────────────────
|
|
30
|
+
server.tool("2kw_get_conversation", "Fetch a conversation by id.", { conversationId: z.string().describe("The conversation ID") }, async ({ conversationId }) => {
|
|
31
|
+
try {
|
|
32
|
+
const { data } = await client.GET("/v1/conversations/{conversationId}", {
|
|
33
|
+
params: { path: { conversationId } },
|
|
34
|
+
});
|
|
35
|
+
return {
|
|
36
|
+
content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
return {
|
|
41
|
+
content: [{ type: "text", text: formatErrorForMcp(error) }],
|
|
42
|
+
isError: true,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
// ── update_conversation ──────────────────────────────────────────────────
|
|
47
|
+
server.tool("2kw_update_conversation", "Replace a conversation's metadata.", {
|
|
48
|
+
conversationId: z.string().describe("The conversation ID"),
|
|
49
|
+
metadata: z.record(z.string(), z.unknown()).optional().describe("Arbitrary metadata (JSON object)"),
|
|
50
|
+
}, async ({ conversationId, metadata }) => {
|
|
51
|
+
try {
|
|
52
|
+
const { data } = await client.POST("/v1/conversations/{conversationId}", {
|
|
53
|
+
params: { path: { conversationId } },
|
|
54
|
+
body: { metadata },
|
|
55
|
+
});
|
|
56
|
+
return {
|
|
57
|
+
content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
return {
|
|
62
|
+
content: [{ type: "text", text: formatErrorForMcp(error) }],
|
|
63
|
+
isError: true,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
// ── delete_conversation ───────────────────────────────────────────────────
|
|
68
|
+
server.tool("2kw_delete_conversation", "Delete a conversation and the responses recorded against it.", { conversationId: z.string().describe("The conversation ID to delete") }, async ({ conversationId }) => {
|
|
69
|
+
try {
|
|
70
|
+
const { data } = await client.DELETE("/v1/conversations/{conversationId}", {
|
|
71
|
+
params: { path: { conversationId } },
|
|
72
|
+
});
|
|
73
|
+
return {
|
|
74
|
+
content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
catch (error) {
|
|
78
|
+
return {
|
|
79
|
+
content: [{ type: "text", text: formatErrorForMcp(error) }],
|
|
80
|
+
isError: true,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
// ── list_conversation_items ──────────────────────────────────────────────
|
|
85
|
+
server.tool("2kw_list_conversation_items", "Fetch a conversation's items in replay order, oldest first.", { conversationId: z.string().describe("The conversation ID") }, async ({ conversationId }) => {
|
|
86
|
+
try {
|
|
87
|
+
const { data } = await client.GET("/v1/conversations/{conversationId}/items", {
|
|
88
|
+
params: { path: { conversationId } },
|
|
89
|
+
});
|
|
90
|
+
return {
|
|
91
|
+
content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
catch (error) {
|
|
95
|
+
return {
|
|
96
|
+
content: [{ type: "text", text: formatErrorForMcp(error) }],
|
|
97
|
+
isError: true,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
//# sourceMappingURL=conversations.js.map
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { readFile, writeFile } 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 FilePurpose = z.enum(["agent_input", "knowledge"]);
|
|
7
|
+
export function register(server, client) {
|
|
8
|
+
// ── list_files ────────────────────────────────────────────────────────────
|
|
9
|
+
server.tool("2kw_list_files", "List the organization's files as a page, newest first by default, optionally narrowed by purpose.", {
|
|
10
|
+
purpose: FilePurpose.optional().describe("Narrow the list to one purpose"),
|
|
11
|
+
page: z.number().optional().default(0).describe("Page number (0-based)"),
|
|
12
|
+
size: z.number().optional().default(20).describe("Page size"),
|
|
13
|
+
}, async ({ purpose, page, size }) => {
|
|
14
|
+
try {
|
|
15
|
+
const { data } = await client.GET("/v1/files", {
|
|
16
|
+
params: { query: { purpose, pageable: { page, size } } },
|
|
17
|
+
});
|
|
18
|
+
const result = data;
|
|
19
|
+
const lines = (result.content ?? []).map((f) => `- ${f.filename} (id: ${f.id}, purpose: ${f.purpose}, bytes: ${f.bytes})`);
|
|
20
|
+
return {
|
|
21
|
+
content: [
|
|
22
|
+
{
|
|
23
|
+
type: "text",
|
|
24
|
+
text: `Files (${result.totalElements} total, page ${(result.number ?? 0) + 1}/${result.totalPages}):\n${lines.join("\n") || "(none)"}`,
|
|
25
|
+
},
|
|
26
|
+
],
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
catch (error) {
|
|
30
|
+
return {
|
|
31
|
+
content: [{ type: "text", text: formatErrorForMcp(error) }],
|
|
32
|
+
isError: true,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
// ── upload_file ───────────────────────────────────────────────────────────
|
|
37
|
+
server.tool("2kw_upload_file", "Upload a local file and get back its file id. purpose=agent_input files expire after the configured TTL unless referenced by a turn; purpose=knowledge is for organization members only and can be ingested into a knowledge base with 2kw_attach_knowledge_document_file. Size and per-end-user quotas apply.", {
|
|
38
|
+
filePath: z.string().describe("Local path to the file to upload"),
|
|
39
|
+
purpose: FilePurpose.describe("Why the file exists — determines its lifecycle"),
|
|
40
|
+
}, async ({ filePath, purpose }) => {
|
|
41
|
+
try {
|
|
42
|
+
const fileBuffer = await readFile(filePath);
|
|
43
|
+
const filename = basename(filePath);
|
|
44
|
+
const mimeType = getMimeType(filename);
|
|
45
|
+
const formData = new FormData();
|
|
46
|
+
formData.append("file", new Blob([fileBuffer], { type: mimeType }), filename);
|
|
47
|
+
const { data } = await client.POST("/v1/files", {
|
|
48
|
+
params: { query: { purpose } },
|
|
49
|
+
// Provide placeholder body for type checking; actual payload comes from bodySerializer
|
|
50
|
+
body: { file: "" },
|
|
51
|
+
bodySerializer: () => formData,
|
|
52
|
+
});
|
|
53
|
+
return {
|
|
54
|
+
content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
return {
|
|
59
|
+
content: [{ type: "text", text: formatErrorForMcp(error) }],
|
|
60
|
+
isError: true,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
// ── get_file ──────────────────────────────────────────────────────────────
|
|
65
|
+
server.tool("2kw_get_file", "Fetch a file's metadata by id.", { fileId: z.string().describe("The file ID") }, async ({ fileId }) => {
|
|
66
|
+
try {
|
|
67
|
+
const { data } = await client.GET("/v1/files/{fileId}", {
|
|
68
|
+
params: { path: { fileId } },
|
|
69
|
+
});
|
|
70
|
+
return {
|
|
71
|
+
content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
catch (error) {
|
|
75
|
+
return {
|
|
76
|
+
content: [{ type: "text", text: formatErrorForMcp(error) }],
|
|
77
|
+
isError: true,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
// ── delete_file ───────────────────────────────────────────────────────────
|
|
82
|
+
server.tool("2kw_delete_file", "Soft-delete an agent_input file. Knowledge files are not deletable through this API; their lifecycle belongs to the knowledge base's document.", { fileId: z.string().describe("The file ID to delete") }, async ({ fileId }) => {
|
|
83
|
+
try {
|
|
84
|
+
const { data } = await client.DELETE("/v1/files/{fileId}", {
|
|
85
|
+
params: { path: { fileId } },
|
|
86
|
+
});
|
|
87
|
+
return {
|
|
88
|
+
content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
catch (error) {
|
|
92
|
+
return {
|
|
93
|
+
content: [{ type: "text", text: formatErrorForMcp(error) }],
|
|
94
|
+
isError: true,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
// ── download_file ─────────────────────────────────────────────────────────
|
|
99
|
+
server.tool("2kw_download_file", "Download a file's bytes and save them to a local path.", {
|
|
100
|
+
fileId: z.string().describe("The file ID"),
|
|
101
|
+
savePath: z.string().describe("Local path to write the downloaded bytes to"),
|
|
102
|
+
}, async ({ fileId, savePath }) => {
|
|
103
|
+
try {
|
|
104
|
+
const { baseUrl, apiKey } = client._config;
|
|
105
|
+
const res = await fetch(`${baseUrl}/v1/files/${encodeURIComponent(fileId)}/content`, {
|
|
106
|
+
headers: { Authorization: `Bearer ${apiKey}` },
|
|
107
|
+
});
|
|
108
|
+
if (!res.ok) {
|
|
109
|
+
let body;
|
|
110
|
+
try {
|
|
111
|
+
body = await res.json();
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
body = {
|
|
115
|
+
error: res.statusText,
|
|
116
|
+
message: `HTTP ${res.status}: ${res.statusText}`,
|
|
117
|
+
status: res.status,
|
|
118
|
+
timestamp: new Date().toISOString(),
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
throw new BackboneApiError(body);
|
|
122
|
+
}
|
|
123
|
+
const buffer = Buffer.from(await res.arrayBuffer());
|
|
124
|
+
await writeFile(savePath, buffer);
|
|
125
|
+
const contentType = res.headers.get("content-type") ?? "application/octet-stream";
|
|
126
|
+
return {
|
|
127
|
+
content: [
|
|
128
|
+
{
|
|
129
|
+
type: "text",
|
|
130
|
+
text: `Downloaded ${buffer.length} bytes (${contentType}) to ${savePath}`,
|
|
131
|
+
},
|
|
132
|
+
],
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
catch (error) {
|
|
136
|
+
return {
|
|
137
|
+
content: [{ type: "text", text: formatErrorForMcp(error) }],
|
|
138
|
+
isError: true,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
//# sourceMappingURL=files.js.map
|