@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/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 VERSION = "0.2.2";
8
- const API_BASE = (process.env.TOKENLAB_API_BASE || "https://api.tokenlab.sh").replace(/\/$/, "");
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
- const scenes = [
12
- "image",
13
- "video",
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 chatMessageSchema = z.object({
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
- const chatCompletionToolSchema = z.object({
37
- type: z.literal("function"),
38
- function: z.object({
39
- name: z.string().min(1),
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
- const server = new McpServer({
46
- name: "tokenlab",
47
- version: VERSION
48
- });
36
+ function definedValues(value) {
37
+ return Object.fromEntries(Object.entries(value).filter(([, entry]) => entry !== undefined));
38
+ }
49
39
 
50
- async function fetchJson(path, options = {}) {
51
- const headers = {
52
- Accept: "application/json",
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
- if (options.body) {
56
- headers["Content-Type"] = "application/json";
57
- }
58
- if (options.auth) {
59
- if (!API_KEY) {
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
- const response = await fetch(`${API_BASE}${path}`, {
66
- method: options.method || "GET",
67
- headers,
68
- body: options.body ? JSON.stringify(options.body) : undefined
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
- const text = await response.text();
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
- if (!response.ok) {
74
- throw new Error(`TokenLab request failed: ${response.status} ${response.statusText}\n${text}`);
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
- return JSON.parse(text);
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 textResult(value) {
81
- return {
82
- content: [
83
- {
84
- type: "text",
85
- text: typeof value === "string" ? value : JSON.stringify(value, null, 2)
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 compactModelDetails(details, pricing) {
92
- return {
93
- id: details.id || details.model || details.name,
94
- object: details.object,
95
- owned_by: details.owned_by,
96
- request_endpoint: details.request_endpoint,
97
- request_shape_mode: details.request_shape_mode,
98
- supported_operations: details.supported_operations,
99
- supported_parameters: details.supported_parameters,
100
- recommended_request: details.recommended_request,
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
- server.tool(
106
- "list_models",
107
- "List public TokenLab models, optionally filtered by recommended task.",
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
- server.tool(
138
- "get_model_pricing",
139
- "Fetch public TokenLab pricing details for one model ID.",
140
- {
141
- model: z.string().min(1).describe("Public TokenLab model ID.")
142
- },
143
- async ({ model }) => {
144
- return textResult(await fetchJson(`/v1/models/${encodeURIComponent(model)}/pricing`));
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
- server.tool(
149
- "compare_models",
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
- return include_raw ? { model, details, pricing } : compactModelDetails(details, pricing);
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
- return textResult({ compared });
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
- server.tool(
173
- "create_chat_completion",
174
- "Create a non-streaming TokenLab OpenAI-compatible Chat Completions call. Requires TOKENLAB_API_KEY.",
175
- {
176
- model: z.string().min(1).describe("Public TokenLab model ID."),
177
- messages: z.array(chatMessageSchema).min(1).describe("OpenAI-compatible conversation messages, including text, image, tool, and function messages."),
178
- temperature: z.number().min(0).max(2).optional().describe("Optional sampling temperature."),
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
- return textResult(await fetchJson("/v1/chat/completions", {
219
- method: "POST",
220
- auth: true,
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
- server.tool(
227
- "create_response",
228
- "Create a TokenLab Responses API call. Requires TOKENLAB_API_KEY.",
229
- {
230
- model: z.string().min(1).describe("Public TokenLab model ID."),
231
- input: z.string().min(1).describe("Responses API input text."),
232
- instructions: z.string().optional().describe("Optional system/developer instructions."),
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
- server.tool(
250
- "create_anthropic_message",
251
- "Create a TokenLab Anthropic Messages call. Requires TOKENLAB_API_KEY.",
252
- {
253
- model: z.string().min(1).describe("Public TokenLab Claude-compatible model ID."),
254
- prompt: z.string().min(1).describe("User prompt text."),
255
- system: z.string().optional().describe("Optional system prompt."),
256
- max_tokens: z.number().int().min(1).max(8192).default(512).describe("Maximum output tokens.")
257
- },
258
- async ({ model, prompt, system, max_tokens }) => {
259
- return textResult(await fetchJson("/v1/messages", {
260
- method: "POST",
261
- auth: true,
262
- body: {
263
- model,
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.tool(
278
- "create_gemini_content",
279
- "Create a TokenLab Gemini generateContent call. Requires TOKENLAB_API_KEY.",
227
+ server.registerTool(
228
+ "compare_models",
280
229
  {
281
- model: z.string().min(1).describe("Public TokenLab Gemini-compatible model ID."),
282
- prompt: z.string().min(1).describe("User prompt text."),
283
- temperature: z.number().min(0).max(2).optional().describe("Optional Gemini generation temperature.")
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 ({ model, prompt, temperature }) => {
286
- return textResult(await fetchJson(`/v1beta/models/${encodeURIComponent(model)}:generateContent`, {
287
- method: "POST",
288
- auth: true,
289
- body: {
290
- contents: [
291
- {
292
- role: "user",
293
- parts: [
294
- {
295
- text: prompt
296
- }
297
- ]
298
- }
299
- ],
300
- ...(temperature === undefined ? {} : {
301
- generationConfig: {
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.tool(
258
+ server.registerTool(
311
259
  "get_api_overview",
312
- "Fetch TokenLab's agent-readable API overview.",
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
- Accept: "text/plain",
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
- }