@2kw/ai-mcp-server 6.1.0 → 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.
package/dist/index.js CHANGED
@@ -18,6 +18,8 @@ import * as datasets from "./tools/datasets.js";
18
18
  import * as experiments from "./tools/experiments.js";
19
19
  import * as evaluators from "./tools/evaluators.js";
20
20
  import * as prompts from "./tools/prompts.js";
21
+ import * as skills from "./tools/skills.js";
22
+ import * as plugins from "./tools/plugins.js";
21
23
  import * as providers from "./tools/providers.js";
22
24
  import * as billing from "./tools/billing.js";
23
25
  import * as scores from "./tools/scores.js";
@@ -83,6 +85,8 @@ async function main() {
83
85
  experiments.register(server, client);
84
86
  evaluators.register(server, client);
85
87
  prompts.register(server, client);
88
+ skills.register(server, client);
89
+ plugins.register(server, client);
86
90
  providers.register(server, client);
87
91
  billing.register(server, client);
88
92
  scores.register(server, client);
@@ -27,6 +27,27 @@ export function register(server, client) {
27
27
  }
28
28
  });
29
29
  // ── get_conversation ─────────────────────────────────────────────────────
30
+ server.tool("2kw_list_conversations", "List conversations, most recently active first. scope 'mine' (default) lists the caller's own; 'org' lists the whole organisation and needs MEMBER or above.", {
31
+ agentId: z.string().optional().describe("Only conversations bound to this agent"),
32
+ scope: z.enum(["mine", "org"]).optional().describe("mine (default) or org"),
33
+ page: z.number().int().min(0).optional().describe("Page number, starting at 0"),
34
+ size: z.number().int().min(1).max(100).optional().describe("Page size, 1 to 100 (default 30)"),
35
+ }, async ({ agentId, scope, page, size }) => {
36
+ try {
37
+ const { data } = await client.GET("/v1/conversations", {
38
+ params: { query: { agent_id: agentId, scope, page, size } },
39
+ });
40
+ return {
41
+ content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
42
+ };
43
+ }
44
+ catch (error) {
45
+ return {
46
+ content: [{ type: "text", text: formatErrorForMcp(error) }],
47
+ isError: true,
48
+ };
49
+ }
50
+ });
30
51
  server.tool("2kw_get_conversation", "Fetch a conversation by id.", { conversationId: z.string().describe("The conversation ID") }, async ({ conversationId }) => {
31
52
  try {
32
53
  const { data } = await client.GET("/v1/conversations/{conversationId}", {
@@ -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
@@ -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
@@ -0,0 +1,177 @@
1
+ import { z } from "zod";
2
+ import { readFile, stat } from "node:fs/promises";
3
+ import { basename, extname } from "node:path";
4
+ import { BackboneApiError, formatErrorForMcp } from "../errors.js";
5
+ // Spring accepts flat pagination parameters despite the generated pageable wrapper.
6
+ function paginationParams(params) {
7
+ return {
8
+ page: params.page ?? 0,
9
+ size: params.size ?? 20,
10
+ sort: params.sort ? [params.sort] : undefined,
11
+ };
12
+ }
13
+ /** Register skill read and import tools (#642). */
14
+ export function register(server, client) {
15
+ server.tool("2kw_list_skills", "List org skills with optional name/status filter and pagination.", {
16
+ search: z.string().optional().describe("Filter by name"),
17
+ status: z.enum(["ACTIVE", "ARCHIVED"]).optional(),
18
+ page: z.number().int().min(0).optional(),
19
+ size: z.number().int().min(1).max(100).optional(),
20
+ sort: z.string().optional().describe("Sort field and direction"),
21
+ }, async (params) => {
22
+ try {
23
+ const { data } = await client.GET("/v1/skills", {
24
+ params: {
25
+ query: {
26
+ search: params.search,
27
+ status: params.status,
28
+ ...paginationParams(params),
29
+ },
30
+ },
31
+ });
32
+ return {
33
+ content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
34
+ };
35
+ }
36
+ catch (error) {
37
+ return {
38
+ content: [{ type: "text", text: formatErrorForMcp(error) }],
39
+ isError: true,
40
+ };
41
+ }
42
+ });
43
+ server.tool("2kw_get_skill", "Get a skill by ID, including its latest version number.", { skillId: z.string() }, async (params) => {
44
+ try {
45
+ const { data } = await client.GET("/v1/skills/{id}", {
46
+ params: { path: { id: params.skillId } },
47
+ });
48
+ return {
49
+ content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
50
+ };
51
+ }
52
+ catch (error) {
53
+ return {
54
+ content: [{ type: "text", text: formatErrorForMcp(error) }],
55
+ isError: true,
56
+ };
57
+ }
58
+ });
59
+ server.tool("2kw_list_skill_versions", "List versions of a skill with pagination.", {
60
+ skillId: z.string(),
61
+ page: z.number().int().min(0).optional(),
62
+ size: z.number().int().min(1).max(100).optional(),
63
+ sort: z.string().optional().describe("Sort field and direction"),
64
+ }, async (params) => {
65
+ try {
66
+ const { data } = await client.GET("/v1/skills/{id}/versions", {
67
+ params: {
68
+ path: { id: params.skillId },
69
+ query: paginationParams(params),
70
+ },
71
+ });
72
+ return {
73
+ content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
74
+ };
75
+ }
76
+ catch (error) {
77
+ return {
78
+ content: [{ type: "text", text: formatErrorForMcp(error) }],
79
+ isError: true,
80
+ };
81
+ }
82
+ });
83
+ server.tool("2kw_get_skill_version", "Get a skill version, including description, body, frontmatter and resources.", {
84
+ skillId: z.string(),
85
+ versionNumber: z.number().int().min(1),
86
+ }, async (params) => {
87
+ try {
88
+ const { data } = await client.GET("/v1/skills/{id}/versions/{versionNumber}", {
89
+ params: { path: { id: params.skillId, versionNumber: params.versionNumber } },
90
+ });
91
+ return {
92
+ content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
93
+ };
94
+ }
95
+ catch (error) {
96
+ return {
97
+ content: [{ type: "text", text: formatErrorForMcp(error) }],
98
+ isError: true,
99
+ };
100
+ }
101
+ });
102
+ server.tool("2kw_list_skill_labels", "List skill labels and the versions they point to.", { skillId: z.string() }, async (params) => {
103
+ try {
104
+ const { data } = await client.GET("/v1/skills/{id}/labels", {
105
+ params: { path: { id: params.skillId } },
106
+ });
107
+ return {
108
+ content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
109
+ };
110
+ }
111
+ catch (error) {
112
+ return {
113
+ content: [{ type: "text", text: formatErrorForMcp(error) }],
114
+ isError: true,
115
+ };
116
+ }
117
+ });
118
+ server.tool("2kw_resolve_skill", "Resolve a skill by name, name@label or name@version number.", { ref: z.string().min(1).describe("Skill reference: name, name@label or name@<n>") }, async (params) => {
119
+ try {
120
+ const { data } = await client.GET("/v1/skills/resolve", {
121
+ params: { query: { ref: params.ref } },
122
+ });
123
+ return {
124
+ content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
125
+ };
126
+ }
127
+ catch (error) {
128
+ return {
129
+ content: [{ type: "text", text: formatErrorForMcp(error) }],
130
+ isError: true,
131
+ };
132
+ }
133
+ });
134
+ server.tool("2kw_import_skill", "Import a local SKILL.md or .zip file. Returns outcome, version, renamedFrom and skipped files. Zip directories before importing.", { filePath: z.string().min(1).describe("Local path to SKILL.md or a .zip archive") }, async (params) => {
135
+ try {
136
+ const info = await stat(params.filePath);
137
+ if (info.isDirectory())
138
+ throw new Error("Cannot import a directory; zip it first.");
139
+ const filename = basename(params.filePath);
140
+ const isZip = extname(filename).toLowerCase() === ".zip";
141
+ if (filename !== "SKILL.md" && !isZip) {
142
+ throw new Error("Import requires a SKILL.md or .zip file.");
143
+ }
144
+ const buffer = await readFile(params.filePath);
145
+ const formData = new FormData();
146
+ formData.append("file", new Blob([buffer], {
147
+ type: isZip ? "application/zip" : "text/markdown",
148
+ }), filename);
149
+ const { baseUrl, apiKey } = client._config;
150
+ const res = await fetch(`${baseUrl.replace(/\/+$/, "")}/v1/skills/import`, {
151
+ method: "POST",
152
+ headers: { Authorization: `Bearer ${apiKey}` },
153
+ body: formData,
154
+ });
155
+ if (!res.ok) {
156
+ const body = await res.json().catch(() => ({}));
157
+ throw new BackboneApiError({
158
+ status: body.status ?? res.status,
159
+ error: body.title ?? body.error ?? res.statusText,
160
+ message: body.detail ?? body.message ?? `HTTP ${res.status}: ${res.statusText}`,
161
+ timestamp: body.timestamp ?? new Date().toISOString(),
162
+ });
163
+ }
164
+ const data = await res.json();
165
+ return {
166
+ content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
167
+ };
168
+ }
169
+ catch (error) {
170
+ return {
171
+ content: [{ type: "text", text: formatErrorForMcp(error) }],
172
+ isError: true,
173
+ };
174
+ }
175
+ });
176
+ }
177
+ //# sourceMappingURL=skills.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@2kw/ai-mcp-server",
3
- "version": "6.1.0",
3
+ "version": "6.2.0-dev.13",
4
4
  "description": "MCP server for 2kw.ai — EU-hosted AI platform: OpenAI-compatible LLM gateway, schema-driven document extraction, transcription, agents with a knowledge base, and cost observability. 158 tools for Claude Code, Cursor, and Windsurf.",
5
5
  "mcpName": "ai.2kw/mcp-server",
6
6
  "keywords": [
@@ -48,7 +48,7 @@
48
48
  "openapi-fetch": "^0.13.5"
49
49
  },
50
50
  "devDependencies": {
51
- "@types/node": "^22.19.17",
51
+ "@types/node": "^22.20.1",
52
52
  "openapi-typescript": "^7.13.0",
53
53
  "tsx": "^4.21.1",
54
54
  "typescript": "^5.9.3"