@tokenlabai/mcp-server 0.3.0 → 0.4.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 +74 -19
- package/contract/mcp-overlay.json +212 -0
- package/contract/openapi.json +12748 -0
- package/generated/tools.json +7188 -0
- package/llms-install.md +60 -0
- package/package.json +15 -3
- package/scripts/generate-contract.mjs +276 -0
- package/scripts/sync-contract.mjs +27 -0
- package/src/index.js +224 -328
- package/.github/workflows/publish-npm.yml +0 -53
- package/server.json +0 -42
- package/test/mcp-tools.test.js +0 -157
package/src/index.js
CHANGED
|
@@ -1,389 +1,285 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
4
|
+
import { mkdir, readFile, stat, writeFile } from "node:fs/promises";
|
|
5
|
+
import { tmpdir } from "node:os";
|
|
6
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
8
|
+
|
|
3
9
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
4
10
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
5
11
|
import { z } from "zod";
|
|
6
12
|
|
|
7
|
-
const
|
|
13
|
+
const root = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
14
|
+
const packageJson = JSON.parse(await readFile(join(root, "package.json"), "utf8"));
|
|
15
|
+
const manifest = JSON.parse(await readFile(join(root, "generated/tools.json"), "utf8"));
|
|
16
|
+
const VERSION = packageJson.version;
|
|
8
17
|
const API_BASE = (process.env.TOKENLAB_API_BASE || "https://api.tokenlab.sh").replace(/\/+$/, "");
|
|
9
18
|
const API_KEY = process.env.TOKENLAB_API_KEY || "";
|
|
10
|
-
const
|
|
11
|
-
const REQUEST_TIMEOUT_MS =
|
|
12
|
-
|
|
13
|
-
|
|
19
|
+
const TOOL_PROFILE = process.env.TOKENLAB_MCP_TOOL_PROFILE || manifest.default_profile;
|
|
20
|
+
const REQUEST_TIMEOUT_MS = positiveInteger(process.env.TOKENLAB_REQUEST_TIMEOUT_MS, 120_000);
|
|
21
|
+
const MAX_FILE_BYTES = positiveInteger(process.env.TOKENLAB_MCP_MAX_FILE_BYTES, 100 * 1024 * 1024);
|
|
22
|
+
const INLINE_BYTES = positiveInteger(process.env.TOKENLAB_MCP_INLINE_BYTES, 2 * 1024 * 1024);
|
|
23
|
+
const ARTIFACT_DIR = resolve(process.env.TOKENLAB_ARTIFACT_DIR || join(tmpdir(), "tokenlab-mcp"));
|
|
24
|
+
|
|
25
|
+
if (!manifest.profiles.includes(TOOL_PROFILE)) {
|
|
26
|
+
throw new Error(`Unknown TOKENLAB_MCP_TOOL_PROFILE '${TOOL_PROFILE}'. Expected ${manifest.profiles.join(" or ")}.`);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const server = new McpServer({ name: "tokenlab", version: VERSION });
|
|
30
|
+
|
|
31
|
+
function positiveInteger(value, fallback) {
|
|
32
|
+
const parsed = Number.parseInt(value || "", 10);
|
|
33
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function definedValues(value) {
|
|
37
|
+
return Object.fromEntries(Object.entries(value).filter(([, entry]) => entry !== undefined));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function textResult(value) {
|
|
41
|
+
return {
|
|
42
|
+
content: [{ type: "text", text: typeof value === "string" ? value : JSON.stringify(value, null, 2) }]
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function requireApiKey(tool) {
|
|
47
|
+
if (tool.auth === "required" && !API_KEY) {
|
|
48
|
+
throw new Error(`TOKENLAB_API_KEY is required for ${tool.name}.`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
14
51
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
"
|
|
22
|
-
|
|
23
|
-
"rerank",
|
|
24
|
-
"translation"
|
|
25
|
-
];
|
|
52
|
+
function appendQuery(url, name, value) {
|
|
53
|
+
if (value === undefined || value === null) return;
|
|
54
|
+
if (Array.isArray(value)) {
|
|
55
|
+
for (const entry of value) appendQuery(url, name, entry);
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
url.searchParams.append(name, typeof value === "object" ? JSON.stringify(value) : String(value));
|
|
59
|
+
}
|
|
26
60
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
tool_call_id: z.string().optional().describe("Tool call ID answered by a tool message.")
|
|
38
|
-
}).passthrough();
|
|
61
|
+
async function appendMultipart(form, name, value, isFile) {
|
|
62
|
+
if (value === undefined || value === null) return;
|
|
63
|
+
if (Array.isArray(value)) {
|
|
64
|
+
for (const entry of value) await appendMultipart(form, name, entry, isFile);
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
if (!isFile) {
|
|
68
|
+
form.append(name, typeof value === "object" ? JSON.stringify(value) : String(value));
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
39
71
|
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
72
|
+
const path = resolve(String(value));
|
|
73
|
+
const details = await stat(path);
|
|
74
|
+
if (!details.isFile()) throw new Error(`${name} must point to a local file.`);
|
|
75
|
+
if (details.size > MAX_FILE_BYTES) {
|
|
76
|
+
throw new Error(`${name} exceeds TOKENLAB_MCP_MAX_FILE_BYTES (${MAX_FILE_BYTES}).`);
|
|
77
|
+
}
|
|
78
|
+
const bytes = await readFile(path);
|
|
79
|
+
form.append(name, new Blob([bytes]), basename(path));
|
|
80
|
+
}
|
|
48
81
|
|
|
49
|
-
|
|
82
|
+
function collectArguments(tool, input) {
|
|
83
|
+
const pathArguments = Object.fromEntries(tool.bindings.path.map((name) => [name, input[name]]));
|
|
84
|
+
const queryArguments = Object.fromEntries(tool.bindings.query.map((name) => [name, input[name]]));
|
|
85
|
+
const headerArguments = Object.fromEntries(tool.bindings.header.map((name) => [name, input[name]]));
|
|
86
|
+
const bodyArguments = tool.bindings.body.includes("body")
|
|
87
|
+
? input.body
|
|
88
|
+
: Object.fromEntries(tool.bindings.body.map((name) => [name, input[name]]));
|
|
89
|
+
return { pathArguments, queryArguments, headerArguments, bodyArguments };
|
|
90
|
+
}
|
|
50
91
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
])
|
|
57
|
-
|
|
92
|
+
function taskAwareResult(tool, response) {
|
|
93
|
+
if (!tool.task || !response || typeof response !== "object") return textResult(response);
|
|
94
|
+
|
|
95
|
+
const statusValue = response[tool.task.status_field];
|
|
96
|
+
const status = typeof statusValue === "string" ? statusValue.toLowerCase() : undefined;
|
|
97
|
+
const taskId = tool.task.id_fields.map((field) => response[field]).find((value) => typeof value === "string" && value);
|
|
98
|
+
const pollUrlValue = response[tool.task.poll_url_field];
|
|
99
|
+
const pollUrl = typeof pollUrlValue === "string" && pollUrlValue
|
|
100
|
+
? pollUrlValue
|
|
101
|
+
: taskId ? `/v1/tasks/${encodeURIComponent(taskId)}` : undefined;
|
|
102
|
+
const terminal = Boolean(status && tool.task.terminal_statuses.includes(status));
|
|
103
|
+
const asyncDelivery = tool.task.mode !== "hybrid" || Boolean(taskId || pollUrl || status);
|
|
104
|
+
|
|
105
|
+
return textResult({
|
|
106
|
+
delivery: asyncDelivery
|
|
107
|
+
? definedValues({
|
|
108
|
+
mode: "async",
|
|
109
|
+
task_id: taskId,
|
|
110
|
+
status,
|
|
111
|
+
poll_url: pollUrl,
|
|
112
|
+
terminal,
|
|
113
|
+
next_tool: terminal ? undefined : "get_task_status"
|
|
114
|
+
})
|
|
115
|
+
: { mode: "complete", terminal: true },
|
|
116
|
+
response
|
|
117
|
+
});
|
|
118
|
+
}
|
|
58
119
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
120
|
+
function extensionFor(mimeType) {
|
|
121
|
+
const known = {
|
|
122
|
+
"audio/mpeg": ".mp3",
|
|
123
|
+
"audio/wav": ".wav",
|
|
124
|
+
"audio/ogg": ".ogg",
|
|
125
|
+
"image/png": ".png",
|
|
126
|
+
"image/jpeg": ".jpg",
|
|
127
|
+
"image/webp": ".webp",
|
|
128
|
+
"video/mp4": ".mp4",
|
|
129
|
+
"application/json": ".json"
|
|
130
|
+
};
|
|
131
|
+
return known[mimeType] || ".bin";
|
|
132
|
+
}
|
|
63
133
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
}
|
|
134
|
+
async function artifactResult(bytes, mimeType, toolName) {
|
|
135
|
+
if (bytes.byteLength <= INLINE_BYTES && mimeType.startsWith("image/")) {
|
|
136
|
+
return { content: [{ type: "image", data: Buffer.from(bytes).toString("base64"), mimeType }] };
|
|
137
|
+
}
|
|
138
|
+
if (bytes.byteLength <= INLINE_BYTES && mimeType.startsWith("audio/")) {
|
|
139
|
+
return { content: [{ type: "audio", data: Buffer.from(bytes).toString("base64"), mimeType }] };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
await mkdir(ARTIFACT_DIR, { recursive: true });
|
|
143
|
+
const digest = createHash("sha256").update(bytes).digest("hex").slice(0, 16);
|
|
144
|
+
const path = join(ARTIFACT_DIR, `${toolName}-${digest}${extensionFor(mimeType)}`);
|
|
145
|
+
await writeFile(path, bytes);
|
|
146
|
+
return textResult({ artifact_path: path, mime_type: mimeType, bytes: bytes.byteLength });
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async function executeGeneratedTool(tool, input) {
|
|
150
|
+
requireApiKey(tool);
|
|
151
|
+
const { pathArguments, queryArguments, headerArguments, bodyArguments } = collectArguments(tool, input);
|
|
152
|
+
let path = tool.path;
|
|
153
|
+
for (const [name, value] of Object.entries(pathArguments)) {
|
|
154
|
+
path = path.replace(`{${name}}`, encodeURIComponent(String(value)));
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const url = new URL(`${API_BASE}${path}`);
|
|
158
|
+
for (const [name, value] of Object.entries(queryArguments)) appendQuery(url, name, value);
|
|
68
159
|
|
|
69
|
-
async function fetchJson(path, options = {}) {
|
|
70
160
|
const headers = {
|
|
71
|
-
Accept: "application/json",
|
|
161
|
+
Accept: "application/json, audio/*, image/*, video/*, application/octet-stream",
|
|
72
162
|
"User-Agent": `tokenlab-mcp-server/${VERSION}`
|
|
73
163
|
};
|
|
74
|
-
|
|
75
|
-
headers[
|
|
164
|
+
for (const [name, value] of Object.entries(headerArguments)) {
|
|
165
|
+
if (value !== undefined) headers[name] = String(value);
|
|
76
166
|
}
|
|
77
|
-
if (
|
|
78
|
-
|
|
79
|
-
|
|
167
|
+
if (API_KEY && tool.auth !== "none") headers.Authorization = `Bearer ${API_KEY}`;
|
|
168
|
+
|
|
169
|
+
let body;
|
|
170
|
+
if (tool.content_type === "application/json") {
|
|
171
|
+
headers["Content-Type"] = "application/json";
|
|
172
|
+
body = JSON.stringify(bodyArguments);
|
|
173
|
+
} else if (tool.content_type === "multipart/form-data") {
|
|
174
|
+
const form = new FormData();
|
|
175
|
+
for (const [name, value] of Object.entries(bodyArguments || {})) {
|
|
176
|
+
await appendMultipart(form, name, value, tool.bindings.files.includes(name));
|
|
80
177
|
}
|
|
81
|
-
|
|
178
|
+
body = form;
|
|
82
179
|
}
|
|
83
180
|
|
|
84
|
-
const response = await fetch(
|
|
85
|
-
method:
|
|
181
|
+
const response = await fetch(url, {
|
|
182
|
+
method: tool.method,
|
|
86
183
|
headers,
|
|
87
|
-
body
|
|
184
|
+
body,
|
|
88
185
|
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
|
|
89
186
|
});
|
|
90
|
-
|
|
91
|
-
const text = await response.text();
|
|
187
|
+
const mimeType = (response.headers.get("content-type") || "application/octet-stream").split(";")[0].trim();
|
|
92
188
|
|
|
93
189
|
if (!response.ok) {
|
|
94
|
-
const detail =
|
|
190
|
+
const detail = (await response.text()).slice(0, 4_000);
|
|
95
191
|
throw new Error(`TokenLab request failed: ${response.status} ${response.statusText}\n${detail}`);
|
|
96
192
|
}
|
|
97
193
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
194
|
+
if (mimeType === "application/json" || mimeType.endsWith("+json")) {
|
|
195
|
+
const result = await response.json();
|
|
196
|
+
const serialized = JSON.stringify(result);
|
|
197
|
+
if (Buffer.byteLength(serialized) > INLINE_BYTES) {
|
|
198
|
+
return artifactResult(Buffer.from(serialized), "application/json", tool.name);
|
|
199
|
+
}
|
|
200
|
+
return taskAwareResult(tool, result);
|
|
102
201
|
}
|
|
202
|
+
if (mimeType.startsWith("text/")) return textResult(await response.text());
|
|
203
|
+
return artifactResult(Buffer.from(await response.arrayBuffer()), mimeType, tool.name);
|
|
103
204
|
}
|
|
104
205
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
206
|
+
const activeTools = manifest.tools.filter((tool) => tool.profiles.includes(TOOL_PROFILE));
|
|
207
|
+
for (const tool of activeTools) {
|
|
208
|
+
const inputSchema = z.fromJSONSchema(tool.input_schema);
|
|
209
|
+
server.registerTool(
|
|
210
|
+
tool.name,
|
|
211
|
+
{
|
|
212
|
+
description: tool.description,
|
|
213
|
+
inputSchema,
|
|
214
|
+
annotations: tool.annotations,
|
|
215
|
+
_meta: {
|
|
216
|
+
"tokenlab/operationId": tool.operation_id,
|
|
217
|
+
"tokenlab/method": tool.method,
|
|
218
|
+
"tokenlab/path": tool.path,
|
|
219
|
+
"tokenlab/contentType": tool.content_type,
|
|
220
|
+
"tokenlab/contractSha256": manifest.source.sha256
|
|
111
221
|
}
|
|
112
|
-
|
|
113
|
-
|
|
222
|
+
},
|
|
223
|
+
async (input) => executeGeneratedTool(tool, input)
|
|
224
|
+
);
|
|
114
225
|
}
|
|
115
226
|
|
|
116
|
-
|
|
117
|
-
return {
|
|
118
|
-
id: details.id || details.model || details.name,
|
|
119
|
-
object: details.object,
|
|
120
|
-
owned_by: details.owned_by,
|
|
121
|
-
request_endpoint: details.request_endpoint,
|
|
122
|
-
request_shape_mode: details.request_shape_mode,
|
|
123
|
-
supported_operations: details.supported_operations,
|
|
124
|
-
supported_parameters: details.supported_parameters,
|
|
125
|
-
recommended_request: details.recommended_request,
|
|
126
|
-
pricing
|
|
127
|
-
};
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
server.tool(
|
|
131
|
-
"list_models",
|
|
132
|
-
"List public TokenLab models, optionally filtered by recommended task.",
|
|
133
|
-
{
|
|
134
|
-
recommended_for: z.enum(scenes).optional().describe("Optional task filter such as image, video, embedding, or rerank."),
|
|
135
|
-
limit: z.number().int().min(1).max(100).default(25).describe("Maximum number of models to return.")
|
|
136
|
-
},
|
|
137
|
-
async ({ recommended_for, limit }) => {
|
|
138
|
-
const query = recommended_for ? `?recommended_for=${encodeURIComponent(recommended_for)}` : "";
|
|
139
|
-
const data = await fetchJson(`/v1/models${query}`);
|
|
140
|
-
const models = Array.isArray(data.data) ? data.data.slice(0, limit) : [];
|
|
141
|
-
|
|
142
|
-
return textResult({
|
|
143
|
-
object: data.object,
|
|
144
|
-
count: Array.isArray(data.data) ? data.data.length : 0,
|
|
145
|
-
returned: models.length,
|
|
146
|
-
models
|
|
147
|
-
});
|
|
148
|
-
}
|
|
149
|
-
);
|
|
150
|
-
|
|
151
|
-
server.tool(
|
|
152
|
-
"get_model",
|
|
153
|
-
"Fetch public TokenLab model details for one model ID.",
|
|
154
|
-
{
|
|
155
|
-
model: z.string().min(1).describe("Public TokenLab model ID, for example gpt-5.5 or gemini-3.5-flash.")
|
|
156
|
-
},
|
|
157
|
-
async ({ model }) => {
|
|
158
|
-
return textResult(await fetchJson(`/v1/models/${encodeURIComponent(model)}`));
|
|
159
|
-
}
|
|
160
|
-
);
|
|
161
|
-
|
|
162
|
-
server.tool(
|
|
163
|
-
"get_model_pricing",
|
|
164
|
-
"Fetch public TokenLab pricing details for one model ID.",
|
|
165
|
-
{
|
|
166
|
-
model: z.string().min(1).describe("Public TokenLab model ID.")
|
|
167
|
-
},
|
|
168
|
-
async ({ model }) => {
|
|
169
|
-
return textResult(await fetchJson(`/v1/models/${encodeURIComponent(model)}/pricing`));
|
|
170
|
-
}
|
|
171
|
-
);
|
|
172
|
-
|
|
173
|
-
server.tool(
|
|
227
|
+
server.registerTool(
|
|
174
228
|
"compare_models",
|
|
175
|
-
"Compare public TokenLab model details and pricing for several model IDs.",
|
|
176
229
|
{
|
|
177
|
-
|
|
178
|
-
|
|
230
|
+
description: "Compare public TokenLab model details and pricing for several model IDs.",
|
|
231
|
+
inputSchema: z.object({
|
|
232
|
+
models: z.array(z.string().min(1)).min(2).max(8),
|
|
233
|
+
include_raw: z.boolean().default(false)
|
|
234
|
+
})
|
|
179
235
|
},
|
|
180
236
|
async ({ models, include_raw }) => {
|
|
181
237
|
const compared = await Promise.all(models.map(async (model) => {
|
|
182
238
|
const encoded = encodeURIComponent(model);
|
|
183
239
|
const [details, pricing] = await Promise.all([
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
error: error.message
|
|
187
|
-
}))
|
|
240
|
+
executePublicJson(`/v1/models/${encoded}`),
|
|
241
|
+
executePublicJson(`/v1/models/${encoded}/pricing`).catch((error) => ({ error: error.message }))
|
|
188
242
|
]);
|
|
189
|
-
|
|
190
|
-
return
|
|
243
|
+
if (include_raw) return { model, details, pricing };
|
|
244
|
+
return {
|
|
245
|
+
id: details.id || details.model || model,
|
|
246
|
+
request_endpoint: details.request_endpoint,
|
|
247
|
+
request_shape_mode: details.request_shape_mode,
|
|
248
|
+
supported_operations: details.supported_operations,
|
|
249
|
+
supported_parameters: details.supported_parameters,
|
|
250
|
+
recommended_request: details.recommended_request,
|
|
251
|
+
pricing
|
|
252
|
+
};
|
|
191
253
|
}));
|
|
192
|
-
|
|
193
254
|
return textResult({ compared });
|
|
194
255
|
}
|
|
195
256
|
);
|
|
196
257
|
|
|
197
|
-
server.
|
|
198
|
-
"
|
|
199
|
-
"Create a non-streaming TokenLab OpenAI-compatible Chat Completions call. Requires TOKENLAB_API_KEY.",
|
|
200
|
-
{
|
|
201
|
-
model: z.string().min(1).describe("Public TokenLab model ID."),
|
|
202
|
-
messages: z.array(chatMessageSchema).min(1).describe("OpenAI-compatible conversation messages, including text, image, tool, and function messages."),
|
|
203
|
-
temperature: z.number().min(0).max(2).optional().describe("Optional sampling temperature."),
|
|
204
|
-
top_p: z.number().min(0).max(1).optional().describe("Optional nucleus sampling probability."),
|
|
205
|
-
n: z.number().int().min(1).max(128).optional().describe("Optional number of non-streaming completions."),
|
|
206
|
-
stop: z.union([z.string(), z.array(z.string()).min(1).max(4)]).optional().describe("Optional stop sequence or up to four stop sequences."),
|
|
207
|
-
max_tokens: z.number().int().min(1).optional().describe("Optional maximum generated tokens."),
|
|
208
|
-
max_completion_tokens: z.number().int().min(1).optional().describe("Optional completion-token cap for compatible reasoning models."),
|
|
209
|
-
presence_penalty: z.number().min(-2).max(2).optional().describe("Optional presence penalty."),
|
|
210
|
-
frequency_penalty: z.number().min(-2).max(2).optional().describe("Optional frequency penalty."),
|
|
211
|
-
tools: z.array(chatCompletionToolSchema).optional().describe("Optional OpenAI function tools available to the model."),
|
|
212
|
-
tool_choice: z.union([
|
|
213
|
-
z.enum(["none", "auto", "required"]),
|
|
214
|
-
z.object({
|
|
215
|
-
type: z.literal("function"),
|
|
216
|
-
function: z.object({ name: z.string().min(1) })
|
|
217
|
-
})
|
|
218
|
-
]).optional().describe("Optional OpenAI tool-choice setting."),
|
|
219
|
-
response_format: z.object({
|
|
220
|
-
type: z.enum(["text", "json_object"])
|
|
221
|
-
}).optional().describe("Optional response format."),
|
|
222
|
-
seed: z.number().int().optional().describe("Optional deterministic seed for compatible models."),
|
|
223
|
-
user: z.string().optional().describe("Optional end-user identifier."),
|
|
224
|
-
parallel_tool_calls: z.boolean().optional().describe("Whether compatible models may make parallel tool calls."),
|
|
225
|
-
reasoning_effort: z.string().optional().describe("Optional reasoning-effort hint for compatible models."),
|
|
226
|
-
logprobs: z.boolean().optional().describe("Whether to return output-token log probabilities."),
|
|
227
|
-
top_logprobs: z.number().int().min(0).max(20).optional().describe("Optional number of likely tokens to include with log probabilities."),
|
|
228
|
-
top_k: z.number().int().min(1).optional().describe("Optional top-k sampling cutoff for compatible models."),
|
|
229
|
-
logit_bias: z.record(z.string(), z.number()).optional().describe("Optional per-token logit-bias map."),
|
|
230
|
-
modalities: z.array(z.string()).min(1).optional().describe("Optional requested output modalities, such as text or audio."),
|
|
231
|
-
audio: z.object({}).passthrough().optional().describe("Optional audio output configuration."),
|
|
232
|
-
prediction: z.object({}).passthrough().optional().describe("Optional prediction hint for compatible models."),
|
|
233
|
-
service_tier: z.string().nullable().optional().describe("Optional service-tier hint for compatible models.")
|
|
234
|
-
},
|
|
235
|
-
async (input) => {
|
|
236
|
-
const body = Object.fromEntries(
|
|
237
|
-
Object.entries({
|
|
238
|
-
...input,
|
|
239
|
-
stream: false
|
|
240
|
-
}).filter(([, value]) => value !== undefined)
|
|
241
|
-
);
|
|
242
|
-
|
|
243
|
-
return textResult(await fetchJson("/v1/chat/completions", {
|
|
244
|
-
method: "POST",
|
|
245
|
-
auth: true,
|
|
246
|
-
body
|
|
247
|
-
}));
|
|
248
|
-
}
|
|
249
|
-
);
|
|
250
|
-
|
|
251
|
-
server.tool(
|
|
252
|
-
"create_response",
|
|
253
|
-
"Create a non-streaming TokenLab Responses API call with text or native structured input. Requires TOKENLAB_API_KEY.",
|
|
254
|
-
{
|
|
255
|
-
model: z.string().min(1).describe("Public TokenLab model ID."),
|
|
256
|
-
input: z.union([
|
|
257
|
-
z.string().min(1),
|
|
258
|
-
z.array(openObjectSchema).min(1)
|
|
259
|
-
]).describe("Responses API input as text or native structured input items."),
|
|
260
|
-
instructions: z.string().optional().describe("Optional system/developer instructions."),
|
|
261
|
-
max_output_tokens: z.number().int().min(1).optional().describe("Optional output token cap."),
|
|
262
|
-
temperature: z.number().optional().describe("Optional sampling temperature."),
|
|
263
|
-
tools: z.array(openObjectSchema).optional().describe("Native Responses API tool definitions."),
|
|
264
|
-
tool_choice: z.union([z.string(), openObjectSchema]).optional().describe("Tool choice policy or explicit tool selection."),
|
|
265
|
-
reasoning_effort: z.string().optional().describe("Reasoning-effort hint for compatible models."),
|
|
266
|
-
include: z.array(z.string()).optional().describe("Additional response sections to include."),
|
|
267
|
-
service_tier: z.string().nullable().optional().describe("Optional service-tier hint."),
|
|
268
|
-
truncation_strategy: z.string().optional().describe("Optional truncation strategy."),
|
|
269
|
-
seed: z.number().int().optional().describe("Optional deterministic seed."),
|
|
270
|
-
user: z.string().optional().describe("Optional end-user identifier."),
|
|
271
|
-
parallel_tool_calls: z.boolean().optional().describe("Whether the model may issue parallel tool calls."),
|
|
272
|
-
metadata: openObjectSchema.optional().describe("Optional request metadata."),
|
|
273
|
-
text: openObjectSchema.optional().describe("Optional native text formatting configuration.")
|
|
274
|
-
},
|
|
275
|
-
async (input) => {
|
|
276
|
-
return textResult(await fetchJson("/v1/responses", {
|
|
277
|
-
method: "POST",
|
|
278
|
-
auth: true,
|
|
279
|
-
body: Object.fromEntries(
|
|
280
|
-
Object.entries({ ...input, stream: false }).filter(([, value]) => value !== undefined)
|
|
281
|
-
)
|
|
282
|
-
}));
|
|
283
|
-
}
|
|
284
|
-
);
|
|
285
|
-
|
|
286
|
-
server.tool(
|
|
287
|
-
"create_anthropic_message",
|
|
288
|
-
"Create a non-streaming TokenLab Anthropic Messages call with native messages, multimodal blocks, and tools. A prompt shortcut remains available for simple calls. Requires TOKENLAB_API_KEY.",
|
|
289
|
-
{
|
|
290
|
-
model: z.string().min(1).describe("Public TokenLab Claude-compatible model ID."),
|
|
291
|
-
messages: z.array(anthropicMessageSchema).min(1).optional().describe("Native Anthropic conversation messages."),
|
|
292
|
-
prompt: z.string().min(1).optional().describe("Convenience shortcut for one user text message; do not combine with messages."),
|
|
293
|
-
system: z.string().optional().describe("Optional system prompt."),
|
|
294
|
-
max_tokens: z.number().int().min(1).default(512).describe("Maximum output tokens."),
|
|
295
|
-
temperature: z.number().min(0).max(1).optional().describe("Optional sampling temperature."),
|
|
296
|
-
top_p: z.number().min(0).max(1).optional().describe("Optional nucleus sampling probability."),
|
|
297
|
-
top_k: z.number().int().min(1).optional().describe("Optional top-k sampling cutoff."),
|
|
298
|
-
stop_sequences: z.array(z.string()).optional().describe("Optional stop sequences."),
|
|
299
|
-
tools: z.array(openObjectSchema).optional().describe("Native Anthropic tool definitions."),
|
|
300
|
-
tool_choice: z.union([z.string(), openObjectSchema]).optional().describe("Tool choice policy or explicit tool selection."),
|
|
301
|
-
metadata: openObjectSchema.optional().describe("Optional request metadata."),
|
|
302
|
-
thinking: openObjectSchema.optional().describe("Thinking configuration for compatible models."),
|
|
303
|
-
service_tier: z.string().optional().describe("Optional service-tier hint.")
|
|
304
|
-
},
|
|
305
|
-
async ({ prompt, messages, ...input }) => {
|
|
306
|
-
if (prompt && messages) {
|
|
307
|
-
throw new Error("Provide either prompt or messages, not both.");
|
|
308
|
-
}
|
|
309
|
-
if (!prompt && !messages) {
|
|
310
|
-
throw new Error("Provide prompt or at least one native Anthropic message.");
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
return textResult(await fetchJson("/v1/messages", {
|
|
314
|
-
method: "POST",
|
|
315
|
-
auth: true,
|
|
316
|
-
body: {
|
|
317
|
-
...input,
|
|
318
|
-
messages: messages || [{ role: "user", content: prompt }],
|
|
319
|
-
stream: false
|
|
320
|
-
}
|
|
321
|
-
}));
|
|
322
|
-
}
|
|
323
|
-
);
|
|
324
|
-
|
|
325
|
-
server.tool(
|
|
326
|
-
"create_gemini_content",
|
|
327
|
-
"Create a TokenLab Gemini generateContent call with native contents, multimodal parts, generation config, and tools. A prompt shortcut remains available for simple calls. Requires TOKENLAB_API_KEY.",
|
|
258
|
+
server.registerTool(
|
|
259
|
+
"get_api_overview",
|
|
328
260
|
{
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
prompt: z.string().min(1).optional().describe("Convenience shortcut for one user text part; do not combine with contents."),
|
|
332
|
-
temperature: z.number().min(0).optional().describe("Convenience temperature setting; do not combine with generationConfig.temperature."),
|
|
333
|
-
systemInstruction: openObjectSchema.optional().describe("Native Gemini system instruction."),
|
|
334
|
-
generationConfig: openObjectSchema.optional().describe("Native Gemini generation configuration."),
|
|
335
|
-
safetySettings: z.array(openObjectSchema).optional().describe("Native Gemini safety settings."),
|
|
336
|
-
tools: z.array(openObjectSchema).optional().describe("Native Gemini tools."),
|
|
337
|
-
toolConfig: openObjectSchema.optional().describe("Native Gemini tool configuration."),
|
|
338
|
-
cachedContent: z.string().optional().describe("Optional cached content resource name.")
|
|
261
|
+
description: "Fetch TokenLab's agent-readable API overview.",
|
|
262
|
+
inputSchema: z.object({})
|
|
339
263
|
},
|
|
340
|
-
async ({ model, prompt, contents, temperature, generationConfig, ...input }) => {
|
|
341
|
-
if (prompt && contents) {
|
|
342
|
-
throw new Error("Provide either prompt or contents, not both.");
|
|
343
|
-
}
|
|
344
|
-
if (!prompt && !contents) {
|
|
345
|
-
throw new Error("Provide prompt or at least one native Gemini content item.");
|
|
346
|
-
}
|
|
347
|
-
if (temperature !== undefined && generationConfig?.temperature !== undefined) {
|
|
348
|
-
throw new Error("Set temperature either directly or in generationConfig, not both.");
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
return textResult(await fetchJson(`/v1beta/models/${encodeURIComponent(model)}:generateContent`, {
|
|
352
|
-
method: "POST",
|
|
353
|
-
auth: true,
|
|
354
|
-
body: {
|
|
355
|
-
...input,
|
|
356
|
-
contents: contents || [{ role: "user", parts: [{ text: prompt }] }],
|
|
357
|
-
...(generationConfig || temperature !== undefined ? {
|
|
358
|
-
generationConfig: {
|
|
359
|
-
...generationConfig,
|
|
360
|
-
...(temperature === undefined ? {} : { temperature })
|
|
361
|
-
}
|
|
362
|
-
} : {})
|
|
363
|
-
}
|
|
364
|
-
}));
|
|
365
|
-
}
|
|
366
|
-
);
|
|
367
|
-
|
|
368
|
-
server.tool(
|
|
369
|
-
"get_api_overview",
|
|
370
|
-
"Fetch TokenLab's agent-readable API overview.",
|
|
371
|
-
{},
|
|
372
264
|
async () => {
|
|
373
265
|
const response = await fetch(`${API_BASE}/llms.txt`, {
|
|
374
|
-
headers: {
|
|
375
|
-
|
|
376
|
-
"User-Agent": `tokenlab-mcp-server/${VERSION}`
|
|
377
|
-
}
|
|
266
|
+
headers: { Accept: "text/plain", "User-Agent": `tokenlab-mcp-server/${VERSION}` },
|
|
267
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
|
|
378
268
|
});
|
|
379
|
-
|
|
380
|
-
if (!response.ok) {
|
|
381
|
-
throw new Error(`TokenLab overview request failed: ${response.status} ${response.statusText}`);
|
|
382
|
-
}
|
|
383
|
-
|
|
269
|
+
if (!response.ok) throw new Error(`TokenLab overview request failed: ${response.status} ${response.statusText}`);
|
|
384
270
|
return textResult(await response.text());
|
|
385
271
|
}
|
|
386
272
|
);
|
|
387
273
|
|
|
274
|
+
async function executePublicJson(path) {
|
|
275
|
+
const response = await fetch(`${API_BASE}${path}`, {
|
|
276
|
+
headers: { Accept: "application/json", "User-Agent": `tokenlab-mcp-server/${VERSION}` },
|
|
277
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
|
|
278
|
+
});
|
|
279
|
+
const text = await response.text();
|
|
280
|
+
if (!response.ok) throw new Error(`TokenLab request failed: ${response.status} ${response.statusText}\n${text.slice(0, 2_000)}`);
|
|
281
|
+
return JSON.parse(text);
|
|
282
|
+
}
|
|
283
|
+
|
|
388
284
|
const transport = new StdioServerTransport();
|
|
389
285
|
await server.connect(transport);
|
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
name: Publish npm package and MCP Registry
|
|
2
|
-
|
|
3
|
-
on:
|
|
4
|
-
push:
|
|
5
|
-
tags:
|
|
6
|
-
- "v*"
|
|
7
|
-
workflow_dispatch:
|
|
8
|
-
|
|
9
|
-
permissions:
|
|
10
|
-
contents: read
|
|
11
|
-
id-token: write
|
|
12
|
-
|
|
13
|
-
jobs:
|
|
14
|
-
publish:
|
|
15
|
-
runs-on: ubuntu-latest
|
|
16
|
-
steps:
|
|
17
|
-
- uses: actions/checkout@v6
|
|
18
|
-
|
|
19
|
-
- uses: actions/setup-node@v6
|
|
20
|
-
if: github.event_name == 'push'
|
|
21
|
-
with:
|
|
22
|
-
node-version: "24"
|
|
23
|
-
registry-url: "https://registry.npmjs.org"
|
|
24
|
-
package-manager-cache: false
|
|
25
|
-
|
|
26
|
-
- name: Verify tag matches package version
|
|
27
|
-
if: github.event_name == 'push'
|
|
28
|
-
run: node --eval 'if (process.env.GITHUB_REF_NAME !== `v${require("./package.json").version}`) process.exit(1)'
|
|
29
|
-
|
|
30
|
-
- name: Install dependencies
|
|
31
|
-
if: github.event_name == 'push'
|
|
32
|
-
run: npm ci
|
|
33
|
-
|
|
34
|
-
- name: Test package
|
|
35
|
-
if: github.event_name == 'push'
|
|
36
|
-
run: npm test
|
|
37
|
-
|
|
38
|
-
- name: Publish npm package
|
|
39
|
-
if: github.event_name == 'push'
|
|
40
|
-
run: npm publish
|
|
41
|
-
|
|
42
|
-
- name: Install MCP Registry publisher
|
|
43
|
-
run: |
|
|
44
|
-
curl -fsSLo mcp-publisher.tar.gz \
|
|
45
|
-
https://github.com/modelcontextprotocol/registry/releases/download/v1.7.9/mcp-publisher_linux_amd64.tar.gz
|
|
46
|
-
echo "ab128162b0616090b47cf245afe0a23f3ef08936fdce19074f5ba0a4469281ac mcp-publisher.tar.gz" | sha256sum --check
|
|
47
|
-
tar -xzf mcp-publisher.tar.gz mcp-publisher
|
|
48
|
-
|
|
49
|
-
- name: Authenticate to MCP Registry
|
|
50
|
-
run: ./mcp-publisher login github-oidc
|
|
51
|
-
|
|
52
|
-
- name: Publish to MCP Registry
|
|
53
|
-
run: ./mcp-publisher publish
|