@tokenlabai/mcp-server 0.2.2 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +80 -22
- package/contract/mcp-overlay.json +212 -0
- package/contract/openapi.json +12782 -0
- package/generated/tools.json +7230 -0
- package/llms-install.md +60 -0
- package/package.json +18 -3
- package/scripts/generate-contract.mjs +276 -0
- package/scripts/sync-contract.mjs +27 -0
- package/src/index.js +227 -273
- package/.github/workflows/publish-npm.yml +0 -29
- package/server.json +0 -36
- package/test/mcp-tools.test.js +0 -78
package/src/index.js
CHANGED
|
@@ -1,331 +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
|
|
8
|
-
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;
|
|
17
|
+
const API_BASE = (process.env.TOKENLAB_API_BASE || "https://api.tokenlab.sh").replace(/\/+$/, "");
|
|
9
18
|
const API_KEY = process.env.TOKENLAB_API_KEY || "";
|
|
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"));
|
|
10
24
|
|
|
11
|
-
|
|
12
|
-
"
|
|
13
|
-
|
|
14
|
-
"music",
|
|
15
|
-
"3d",
|
|
16
|
-
"tts",
|
|
17
|
-
"stt",
|
|
18
|
-
"embedding",
|
|
19
|
-
"rerank",
|
|
20
|
-
"translation"
|
|
21
|
-
];
|
|
25
|
+
if (!manifest.profiles.includes(TOOL_PROFILE)) {
|
|
26
|
+
throw new Error(`Unknown TOKENLAB_MCP_TOOL_PROFILE '${TOOL_PROFILE}'. Expected ${manifest.profiles.join(" or ")}.`);
|
|
27
|
+
}
|
|
22
28
|
|
|
23
|
-
const
|
|
24
|
-
role: z.enum(["system", "user", "assistant", "function", "tool", "developer"])
|
|
25
|
-
.describe("OpenAI Chat Completions message role."),
|
|
26
|
-
content: z.union([
|
|
27
|
-
z.string(),
|
|
28
|
-
z.array(z.object({}).passthrough()),
|
|
29
|
-
z.null()
|
|
30
|
-
]).optional().describe("Text, OpenAI-compatible multimodal content parts, or null for tool/function messages."),
|
|
31
|
-
name: z.string().optional().describe("Optional name for a function or tool message."),
|
|
32
|
-
tool_calls: z.array(z.object({}).passthrough()).optional().describe("Tool calls made by an assistant message."),
|
|
33
|
-
tool_call_id: z.string().optional().describe("Tool call ID answered by a tool message.")
|
|
34
|
-
}).passthrough();
|
|
29
|
+
const server = new McpServer({ name: "tokenlab", version: VERSION });
|
|
35
30
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
description: z.string().optional(),
|
|
41
|
-
parameters: z.object({}).passthrough().optional()
|
|
42
|
-
}).passthrough()
|
|
43
|
-
}).passthrough();
|
|
31
|
+
function positiveInteger(value, fallback) {
|
|
32
|
+
const parsed = Number.parseInt(value || "", 10);
|
|
33
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
|
|
34
|
+
}
|
|
44
35
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
});
|
|
36
|
+
function definedValues(value) {
|
|
37
|
+
return Object.fromEntries(Object.entries(value).filter(([, entry]) => entry !== undefined));
|
|
38
|
+
}
|
|
49
39
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
"User-Agent": `tokenlab-mcp-server/${VERSION}`
|
|
40
|
+
function textResult(value) {
|
|
41
|
+
return {
|
|
42
|
+
content: [{ type: "text", text: typeof value === "string" ? value : JSON.stringify(value, null, 2) }]
|
|
54
43
|
};
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
if (
|
|
59
|
-
|
|
60
|
-
throw new Error("TOKENLAB_API_KEY is required for TokenLab inference tools.");
|
|
61
|
-
}
|
|
62
|
-
headers.Authorization = `Bearer ${API_KEY}`;
|
|
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}.`);
|
|
63
49
|
}
|
|
50
|
+
}
|
|
64
51
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
+
}
|
|
70
60
|
|
|
71
|
-
|
|
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
|
+
}
|
|
72
71
|
|
|
73
|
-
|
|
74
|
-
|
|
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}).`);
|
|
75
77
|
}
|
|
78
|
+
const bytes = await readFile(path);
|
|
79
|
+
form.append(name, new Blob([bytes]), basename(path));
|
|
80
|
+
}
|
|
76
81
|
|
|
77
|
-
|
|
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 };
|
|
78
90
|
}
|
|
79
91
|
|
|
80
|
-
function
|
|
81
|
-
return
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
+
});
|
|
89
118
|
}
|
|
90
119
|
|
|
91
|
-
function
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
pricing
|
|
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"
|
|
102
130
|
};
|
|
131
|
+
return known[mimeType] || ".bin";
|
|
103
132
|
}
|
|
104
133
|
|
|
105
|
-
|
|
106
|
-
"
|
|
107
|
-
|
|
108
|
-
{
|
|
109
|
-
recommended_for: z.enum(scenes).optional().describe("Optional task filter such as image, video, embedding, or rerank."),
|
|
110
|
-
limit: z.number().int().min(1).max(100).default(25).describe("Maximum number of models to return.")
|
|
111
|
-
},
|
|
112
|
-
async ({ recommended_for, limit }) => {
|
|
113
|
-
const query = recommended_for ? `?recommended_for=${encodeURIComponent(recommended_for)}` : "";
|
|
114
|
-
const data = await fetchJson(`/v1/models${query}`);
|
|
115
|
-
const models = Array.isArray(data.data) ? data.data.slice(0, limit) : [];
|
|
116
|
-
|
|
117
|
-
return textResult({
|
|
118
|
-
object: data.object,
|
|
119
|
-
count: Array.isArray(data.data) ? data.data.length : 0,
|
|
120
|
-
returned: models.length,
|
|
121
|
-
models
|
|
122
|
-
});
|
|
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 }] };
|
|
123
137
|
}
|
|
124
|
-
)
|
|
125
|
-
|
|
126
|
-
server.tool(
|
|
127
|
-
"get_model",
|
|
128
|
-
"Fetch public TokenLab model details for one model ID.",
|
|
129
|
-
{
|
|
130
|
-
model: z.string().min(1).describe("Public TokenLab model ID, for example gpt-5.5 or gemini-3.5-flash.")
|
|
131
|
-
},
|
|
132
|
-
async ({ model }) => {
|
|
133
|
-
return textResult(await fetchJson(`/v1/models/${encodeURIComponent(model)}`));
|
|
138
|
+
if (bytes.byteLength <= INLINE_BYTES && mimeType.startsWith("audio/")) {
|
|
139
|
+
return { content: [{ type: "audio", data: Buffer.from(bytes).toString("base64"), mimeType }] };
|
|
134
140
|
}
|
|
135
|
-
);
|
|
136
141
|
|
|
137
|
-
|
|
138
|
-
"
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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)));
|
|
145
155
|
}
|
|
146
|
-
);
|
|
147
156
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
"Compare public TokenLab model details and pricing for several model IDs.",
|
|
151
|
-
{
|
|
152
|
-
models: z.array(z.string().min(1)).min(2).max(8).describe("Public TokenLab model IDs to compare."),
|
|
153
|
-
include_raw: z.boolean().default(false).describe("Return raw details and pricing payloads instead of compact summaries.")
|
|
154
|
-
},
|
|
155
|
-
async ({ models, include_raw }) => {
|
|
156
|
-
const compared = await Promise.all(models.map(async (model) => {
|
|
157
|
-
const encoded = encodeURIComponent(model);
|
|
158
|
-
const [details, pricing] = await Promise.all([
|
|
159
|
-
fetchJson(`/v1/models/${encoded}`),
|
|
160
|
-
fetchJson(`/v1/models/${encoded}/pricing`).catch((error) => ({
|
|
161
|
-
error: error.message
|
|
162
|
-
}))
|
|
163
|
-
]);
|
|
157
|
+
const url = new URL(`${API_BASE}${path}`);
|
|
158
|
+
for (const [name, value] of Object.entries(queryArguments)) appendQuery(url, name, value);
|
|
164
159
|
|
|
165
|
-
|
|
166
|
-
|
|
160
|
+
const headers = {
|
|
161
|
+
Accept: "application/json, audio/*, image/*, video/*, application/octet-stream",
|
|
162
|
+
"User-Agent": `tokenlab-mcp-server/${VERSION}`
|
|
163
|
+
};
|
|
164
|
+
for (const [name, value] of Object.entries(headerArguments)) {
|
|
165
|
+
if (value !== undefined) headers[name] = String(value);
|
|
166
|
+
}
|
|
167
|
+
if (API_KEY && tool.auth !== "none") headers.Authorization = `Bearer ${API_KEY}`;
|
|
167
168
|
|
|
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));
|
|
177
|
+
}
|
|
178
|
+
body = form;
|
|
169
179
|
}
|
|
170
|
-
);
|
|
171
180
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
top_p: z.number().min(0).max(1).optional().describe("Optional nucleus sampling probability."),
|
|
180
|
-
n: z.number().int().min(1).max(128).optional().describe("Optional number of non-streaming completions."),
|
|
181
|
-
stop: z.union([z.string(), z.array(z.string()).min(1).max(4)]).optional().describe("Optional stop sequence or up to four stop sequences."),
|
|
182
|
-
max_tokens: z.number().int().min(1).optional().describe("Optional maximum generated tokens."),
|
|
183
|
-
max_completion_tokens: z.number().int().min(1).optional().describe("Optional completion-token cap for compatible reasoning models."),
|
|
184
|
-
presence_penalty: z.number().min(-2).max(2).optional().describe("Optional presence penalty."),
|
|
185
|
-
frequency_penalty: z.number().min(-2).max(2).optional().describe("Optional frequency penalty."),
|
|
186
|
-
tools: z.array(chatCompletionToolSchema).optional().describe("Optional OpenAI function tools available to the model."),
|
|
187
|
-
tool_choice: z.union([
|
|
188
|
-
z.enum(["none", "auto", "required"]),
|
|
189
|
-
z.object({
|
|
190
|
-
type: z.literal("function"),
|
|
191
|
-
function: z.object({ name: z.string().min(1) })
|
|
192
|
-
})
|
|
193
|
-
]).optional().describe("Optional OpenAI tool-choice setting."),
|
|
194
|
-
response_format: z.object({
|
|
195
|
-
type: z.enum(["text", "json_object"])
|
|
196
|
-
}).optional().describe("Optional response format."),
|
|
197
|
-
seed: z.number().int().optional().describe("Optional deterministic seed for compatible models."),
|
|
198
|
-
user: z.string().optional().describe("Optional end-user identifier."),
|
|
199
|
-
parallel_tool_calls: z.boolean().optional().describe("Whether compatible models may make parallel tool calls."),
|
|
200
|
-
reasoning_effort: z.string().optional().describe("Optional reasoning-effort hint for compatible models."),
|
|
201
|
-
logprobs: z.boolean().optional().describe("Whether to return output-token log probabilities."),
|
|
202
|
-
top_logprobs: z.number().int().min(0).max(20).optional().describe("Optional number of likely tokens to include with log probabilities."),
|
|
203
|
-
top_k: z.number().int().min(1).optional().describe("Optional top-k sampling cutoff for compatible models."),
|
|
204
|
-
logit_bias: z.record(z.string(), z.number()).optional().describe("Optional per-token logit-bias map."),
|
|
205
|
-
modalities: z.array(z.string()).min(1).optional().describe("Optional requested output modalities, such as text or audio."),
|
|
206
|
-
audio: z.object({}).passthrough().optional().describe("Optional audio output configuration."),
|
|
207
|
-
prediction: z.object({}).passthrough().optional().describe("Optional prediction hint for compatible models."),
|
|
208
|
-
service_tier: z.string().nullable().optional().describe("Optional service-tier hint for compatible models.")
|
|
209
|
-
},
|
|
210
|
-
async (input) => {
|
|
211
|
-
const body = Object.fromEntries(
|
|
212
|
-
Object.entries({
|
|
213
|
-
...input,
|
|
214
|
-
stream: false
|
|
215
|
-
}).filter(([, value]) => value !== undefined)
|
|
216
|
-
);
|
|
181
|
+
const response = await fetch(url, {
|
|
182
|
+
method: tool.method,
|
|
183
|
+
headers,
|
|
184
|
+
body,
|
|
185
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
|
|
186
|
+
});
|
|
187
|
+
const mimeType = (response.headers.get("content-type") || "application/octet-stream").split(";")[0].trim();
|
|
217
188
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
body
|
|
222
|
-
}));
|
|
189
|
+
if (!response.ok) {
|
|
190
|
+
const detail = (await response.text()).slice(0, 4_000);
|
|
191
|
+
throw new Error(`TokenLab request failed: ${response.status} ${response.statusText}\n${detail}`);
|
|
223
192
|
}
|
|
224
|
-
);
|
|
225
193
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
max_output_tokens: z.number().int().min(1).max(8192).optional().describe("Optional output token cap.")
|
|
234
|
-
},
|
|
235
|
-
async ({ model, input, instructions, max_output_tokens }) => {
|
|
236
|
-
return textResult(await fetchJson("/v1/responses", {
|
|
237
|
-
method: "POST",
|
|
238
|
-
auth: true,
|
|
239
|
-
body: {
|
|
240
|
-
model,
|
|
241
|
-
input,
|
|
242
|
-
...(instructions ? { instructions } : {}),
|
|
243
|
-
...(max_output_tokens ? { max_output_tokens } : {})
|
|
244
|
-
}
|
|
245
|
-
}));
|
|
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);
|
|
246
201
|
}
|
|
247
|
-
);
|
|
202
|
+
if (mimeType.startsWith("text/")) return textResult(await response.text());
|
|
203
|
+
return artifactResult(Buffer.from(await response.arrayBuffer()), mimeType, tool.name);
|
|
204
|
+
}
|
|
248
205
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
max_tokens,
|
|
265
|
-
...(system ? { system } : {}),
|
|
266
|
-
messages: [
|
|
267
|
-
{
|
|
268
|
-
role: "user",
|
|
269
|
-
content: prompt
|
|
270
|
-
}
|
|
271
|
-
]
|
|
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
|
|
272
221
|
}
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
);
|
|
222
|
+
},
|
|
223
|
+
async (input) => executeGeneratedTool(tool, input)
|
|
224
|
+
);
|
|
225
|
+
}
|
|
276
226
|
|
|
277
|
-
server.
|
|
278
|
-
"
|
|
279
|
-
"Create a TokenLab Gemini generateContent call. Requires TOKENLAB_API_KEY.",
|
|
227
|
+
server.registerTool(
|
|
228
|
+
"compare_models",
|
|
280
229
|
{
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
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
|
+
})
|
|
284
235
|
},
|
|
285
|
-
async ({
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
temperature
|
|
303
|
-
}
|
|
304
|
-
})
|
|
305
|
-
}
|
|
236
|
+
async ({ models, include_raw }) => {
|
|
237
|
+
const compared = await Promise.all(models.map(async (model) => {
|
|
238
|
+
const encoded = encodeURIComponent(model);
|
|
239
|
+
const [details, pricing] = await Promise.all([
|
|
240
|
+
executePublicJson(`/v1/models/${encoded}`),
|
|
241
|
+
executePublicJson(`/v1/models/${encoded}/pricing`).catch((error) => ({ error: error.message }))
|
|
242
|
+
]);
|
|
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
|
+
};
|
|
306
253
|
}));
|
|
254
|
+
return textResult({ compared });
|
|
307
255
|
}
|
|
308
256
|
);
|
|
309
257
|
|
|
310
|
-
server.
|
|
258
|
+
server.registerTool(
|
|
311
259
|
"get_api_overview",
|
|
312
|
-
|
|
313
|
-
|
|
260
|
+
{
|
|
261
|
+
description: "Fetch TokenLab's agent-readable API overview.",
|
|
262
|
+
inputSchema: z.object({})
|
|
263
|
+
},
|
|
314
264
|
async () => {
|
|
315
265
|
const response = await fetch(`${API_BASE}/llms.txt`, {
|
|
316
|
-
headers: {
|
|
317
|
-
|
|
318
|
-
"User-Agent": `tokenlab-mcp-server/${VERSION}`
|
|
319
|
-
}
|
|
266
|
+
headers: { Accept: "text/plain", "User-Agent": `tokenlab-mcp-server/${VERSION}` },
|
|
267
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
|
|
320
268
|
});
|
|
321
|
-
|
|
322
|
-
if (!response.ok) {
|
|
323
|
-
throw new Error(`TokenLab overview request failed: ${response.status} ${response.statusText}`);
|
|
324
|
-
}
|
|
325
|
-
|
|
269
|
+
if (!response.ok) throw new Error(`TokenLab overview request failed: ${response.status} ${response.statusText}`);
|
|
326
270
|
return textResult(await response.text());
|
|
327
271
|
}
|
|
328
272
|
);
|
|
329
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
|
+
|
|
330
284
|
const transport = new StdioServerTransport();
|
|
331
285
|
await server.connect(transport);
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
name: Publish npm package
|
|
2
|
-
|
|
3
|
-
on:
|
|
4
|
-
push:
|
|
5
|
-
tags:
|
|
6
|
-
- "v*"
|
|
7
|
-
|
|
8
|
-
permissions:
|
|
9
|
-
contents: read
|
|
10
|
-
id-token: write
|
|
11
|
-
|
|
12
|
-
jobs:
|
|
13
|
-
publish:
|
|
14
|
-
runs-on: ubuntu-latest
|
|
15
|
-
steps:
|
|
16
|
-
- uses: actions/checkout@v6
|
|
17
|
-
|
|
18
|
-
- uses: actions/setup-node@v6
|
|
19
|
-
with:
|
|
20
|
-
node-version: "24"
|
|
21
|
-
registry-url: "https://registry.npmjs.org"
|
|
22
|
-
package-manager-cache: false
|
|
23
|
-
|
|
24
|
-
- name: Verify tag matches package version
|
|
25
|
-
run: node --eval 'if (process.env.GITHUB_REF_NAME !== `v${require("./package.json").version}`) process.exit(1)'
|
|
26
|
-
|
|
27
|
-
- run: npm ci
|
|
28
|
-
- run: npm test
|
|
29
|
-
- run: npm publish
|
package/server.json
DELETED
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
|
|
3
|
-
"name": "io.github.hedging8563/tokenlab",
|
|
4
|
-
"title": "TokenLab",
|
|
5
|
-
"description": "MCP server for TokenLab model discovery, pricing, OpenAI-compatible Chat Completions, native endpoints, and inference helpers.",
|
|
6
|
-
"repository": {
|
|
7
|
-
"url": "https://github.com/hedging8563/tokenlab-mcp-server",
|
|
8
|
-
"source": "github"
|
|
9
|
-
},
|
|
10
|
-
"version": "0.2.2",
|
|
11
|
-
"packages": [
|
|
12
|
-
{
|
|
13
|
-
"registryType": "npm",
|
|
14
|
-
"identifier": "@tokenlabai/mcp-server",
|
|
15
|
-
"version": "0.2.2",
|
|
16
|
-
"transport": {
|
|
17
|
-
"type": "stdio"
|
|
18
|
-
},
|
|
19
|
-
"environmentVariables": [
|
|
20
|
-
{
|
|
21
|
-
"name": "TOKENLAB_API_BASE",
|
|
22
|
-
"description": "Optional TokenLab API base URL. Defaults to https://api.tokenlab.sh.",
|
|
23
|
-
"isRequired": false,
|
|
24
|
-
"format": "string"
|
|
25
|
-
},
|
|
26
|
-
{
|
|
27
|
-
"name": "TOKENLAB_API_KEY",
|
|
28
|
-
"description": "Optional TokenLab API key. Required only for inference tools such as create_chat_completion, create_response, create_anthropic_message, and create_gemini_content.",
|
|
29
|
-
"isRequired": false,
|
|
30
|
-
"format": "string",
|
|
31
|
-
"isSecret": true
|
|
32
|
-
}
|
|
33
|
-
]
|
|
34
|
-
}
|
|
35
|
-
]
|
|
36
|
-
}
|