@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/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 VERSION = "0.3.0";
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 configuredTimeoutMs = Number.parseInt(process.env.TOKENLAB_REQUEST_TIMEOUT_MS || "30000", 10);
11
- const REQUEST_TIMEOUT_MS = Number.isFinite(configuredTimeoutMs) && configuredTimeoutMs > 0
12
- ? configuredTimeoutMs
13
- : 30_000;
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
- const scenes = [
16
- "image",
17
- "video",
18
- "music",
19
- "3d",
20
- "tts",
21
- "stt",
22
- "embedding",
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
- const chatMessageSchema = z.object({
28
- role: z.enum(["system", "user", "assistant", "function", "tool", "developer"])
29
- .describe("OpenAI Chat Completions message role."),
30
- content: z.union([
31
- z.string(),
32
- z.array(z.object({}).passthrough()),
33
- z.null()
34
- ]).optional().describe("Text, OpenAI-compatible multimodal content parts, or null for tool/function messages."),
35
- name: z.string().optional().describe("Optional name for a function or tool message."),
36
- tool_calls: z.array(z.object({}).passthrough()).optional().describe("Tool calls made by an assistant message."),
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 chatCompletionToolSchema = z.object({
41
- type: z.literal("function"),
42
- function: z.object({
43
- name: z.string().min(1),
44
- description: z.string().optional(),
45
- parameters: z.object({}).passthrough().optional()
46
- }).passthrough()
47
- }).passthrough();
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
- const openObjectSchema = z.object({}).passthrough();
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
- const anthropicMessageSchema = z.object({
52
- role: z.enum(["user", "assistant"]),
53
- content: z.union([
54
- z.string(),
55
- z.array(openObjectSchema).min(1)
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
- const geminiContentSchema = z.object({
60
- role: z.enum(["user", "model"]).optional(),
61
- parts: z.array(openObjectSchema).min(1)
62
- }).passthrough();
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
- const server = new McpServer({
65
- name: "tokenlab",
66
- version: VERSION
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
- if (options.body) {
75
- headers["Content-Type"] = "application/json";
164
+ for (const [name, value] of Object.entries(headerArguments)) {
165
+ if (value !== undefined) headers[name] = String(value);
76
166
  }
77
- if (options.auth) {
78
- if (!API_KEY) {
79
- throw new Error("TOKENLAB_API_KEY is required for TokenLab inference tools.");
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
- headers.Authorization = `Bearer ${API_KEY}`;
178
+ body = form;
82
179
  }
83
180
 
84
- const response = await fetch(`${API_BASE}${path}`, {
85
- method: options.method || "GET",
181
+ const response = await fetch(url, {
182
+ method: tool.method,
86
183
  headers,
87
- body: options.body ? JSON.stringify(options.body) : undefined,
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 = text.length > 2_000 ? `${text.slice(0, 2_000)}...` : text;
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
- try {
99
- return JSON.parse(text);
100
- } catch {
101
- throw new Error("TokenLab returned a successful response that was not valid JSON.");
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
- function textResult(value) {
106
- return {
107
- content: [
108
- {
109
- type: "text",
110
- text: typeof value === "string" ? value : JSON.stringify(value, null, 2)
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
- function compactModelDetails(details, pricing) {
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
- models: z.array(z.string().min(1)).min(2).max(8).describe("Public TokenLab model IDs to compare."),
178
- include_raw: z.boolean().default(false).describe("Return raw details and pricing payloads instead of compact summaries.")
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
- fetchJson(`/v1/models/${encoded}`),
185
- fetchJson(`/v1/models/${encoded}/pricing`).catch((error) => ({
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 include_raw ? { model, details, pricing } : compactModelDetails(details, pricing);
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.tool(
198
- "create_chat_completion",
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
- model: z.string().min(1).describe("Public TokenLab Gemini-compatible model ID."),
330
- contents: z.array(geminiContentSchema).min(1).optional().describe("Native Gemini conversation contents."),
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
- Accept: "text/plain",
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