@tokenlabai/mcp-server 0.2.0 → 0.2.2
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/.github/workflows/publish-npm.yml +29 -0
- package/README.md +10 -8
- package/package.json +5 -5
- package/server.json +4 -4
- package/src/index.js +77 -1
- package/test/mcp-tools.test.js +78 -0
|
@@ -0,0 +1,29 @@
|
|
|
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/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# TokenLab MCP Server
|
|
2
2
|
|
|
3
|
-
Model Context Protocol server for TokenLab public model discovery, pricing, native endpoint guidance, and optional inference helpers.
|
|
3
|
+
Model Context Protocol server for TokenLab public model discovery, pricing, OpenAI-compatible Chat Completions, native endpoint guidance, and optional inference helpers.
|
|
4
4
|
|
|
5
5
|
It exposes public catalog tools for agents that need to choose models, inspect supported request formats, or compare pricing before calling TokenLab APIs. Optional inference tools require `TOKENLAB_API_KEY`.
|
|
6
6
|
|
|
@@ -11,6 +11,7 @@ It exposes public catalog tools for agents that need to choose models, inspect s
|
|
|
11
11
|
- `get_model_pricing` - Fetch pricing details for one model ID.
|
|
12
12
|
- `compare_models` - Compare details and pricing for several model IDs.
|
|
13
13
|
- `get_api_overview` - Fetch the agent-readable `llms.txt` overview.
|
|
14
|
+
- `create_chat_completion` - Call TokenLab's OpenAI-compatible non-streaming Chat Completions API. Requires `TOKENLAB_API_KEY`.
|
|
14
15
|
- `create_response` - Call TokenLab Responses API. Requires `TOKENLAB_API_KEY`.
|
|
15
16
|
- `create_anthropic_message` - Call TokenLab Anthropic Messages API. Requires `TOKENLAB_API_KEY`.
|
|
16
17
|
- `create_gemini_content` - Call TokenLab Gemini generateContent API. Requires `TOKENLAB_API_KEY`.
|
|
@@ -44,28 +45,29 @@ Claude Desktop style config:
|
|
|
44
45
|
}
|
|
45
46
|
```
|
|
46
47
|
|
|
47
|
-
No TokenLab API key is required for the public catalog tools. Set `TOKENLAB_API_KEY` only when you want the inference helper tools to call paid TokenLab APIs.
|
|
48
|
+
No TokenLab API key is required for the public catalog tools. Set `TOKENLAB_API_KEY` only when you want the inference helper tools to call paid TokenLab APIs. `create_chat_completion` supports OpenAI-compatible messages, multimodal content parts, function calling, and common generation controls. MCP tools return a normal JSON result, so streaming is intentionally disabled.
|
|
48
49
|
|
|
49
50
|
## Environment
|
|
50
51
|
|
|
51
52
|
- `TOKENLAB_API_BASE`: optional, defaults to `https://api.tokenlab.sh`
|
|
52
|
-
- `TOKENLAB_API_KEY`: optional; required only for `create_response`, `create_anthropic_message`, and `create_gemini_content`
|
|
53
|
+
- `TOKENLAB_API_KEY`: optional; required only for `create_chat_completion`, `create_response`, `create_anthropic_message`, and `create_gemini_content`
|
|
53
54
|
|
|
54
55
|
## MCP Registry Metadata
|
|
55
56
|
|
|
56
57
|
This repository includes `server.json` for the official MCP Registry.
|
|
57
58
|
|
|
58
|
-
|
|
59
|
+
Current publication:
|
|
59
60
|
|
|
60
|
-
- npm package: `@tokenlabai/mcp-server`
|
|
61
|
+
- npm package: `@tokenlabai/mcp-server@0.2.2`
|
|
61
62
|
- MCP registry name: `io.github.hedging8563/tokenlab`
|
|
63
|
+
- Official MCP Registry status: active
|
|
62
64
|
- `package.json.mcpName`: `io.github.hedging8563/tokenlab`
|
|
63
65
|
|
|
64
|
-
|
|
66
|
+
For a new release:
|
|
65
67
|
|
|
66
|
-
1. Publish `@tokenlabai/mcp-server` to npm.
|
|
68
|
+
1. Publish the next `@tokenlabai/mcp-server` version to npm.
|
|
67
69
|
2. Authenticate with `mcp-publisher login github`.
|
|
68
|
-
3.
|
|
70
|
+
3. Update `server.json` and run `mcp-publisher publish`.
|
|
69
71
|
|
|
70
72
|
## Links
|
|
71
73
|
|
package/package.json
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tokenlabai/mcp-server",
|
|
3
|
-
"version": "0.2.
|
|
4
|
-
"description": "MCP server for TokenLab model discovery, pricing, native endpoint guidance, and optional inference helpers.",
|
|
3
|
+
"version": "0.2.2",
|
|
4
|
+
"description": "MCP server for TokenLab model discovery, pricing, OpenAI-compatible Chat Completions, native endpoint guidance, and optional inference helpers.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"mcpName": "io.github.hedging8563/tokenlab",
|
|
7
7
|
"bin": {
|
|
8
|
-
"tokenlab-mcp-server": "
|
|
8
|
+
"tokenlab-mcp-server": "src/index.js"
|
|
9
9
|
},
|
|
10
10
|
"scripts": {
|
|
11
11
|
"start": "node ./src/index.js",
|
|
12
|
-
"test": "node --check ./src/index.js"
|
|
12
|
+
"test": "node --check ./src/index.js && node --test"
|
|
13
13
|
},
|
|
14
14
|
"keywords": [
|
|
15
15
|
"mcp",
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
},
|
|
31
31
|
"repository": {
|
|
32
32
|
"type": "git",
|
|
33
|
-
"url": "https://github.com/hedging8563/tokenlab-mcp-server.git"
|
|
33
|
+
"url": "git+https://github.com/hedging8563/tokenlab-mcp-server.git"
|
|
34
34
|
},
|
|
35
35
|
"homepage": "https://docs.tokenlab.sh"
|
|
36
36
|
}
|
package/server.json
CHANGED
|
@@ -2,17 +2,17 @@
|
|
|
2
2
|
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
|
|
3
3
|
"name": "io.github.hedging8563/tokenlab",
|
|
4
4
|
"title": "TokenLab",
|
|
5
|
-
"description": "MCP server for TokenLab model discovery, pricing,
|
|
5
|
+
"description": "MCP server for TokenLab model discovery, pricing, OpenAI-compatible Chat Completions, native endpoints, and inference helpers.",
|
|
6
6
|
"repository": {
|
|
7
7
|
"url": "https://github.com/hedging8563/tokenlab-mcp-server",
|
|
8
8
|
"source": "github"
|
|
9
9
|
},
|
|
10
|
-
"version": "0.2.
|
|
10
|
+
"version": "0.2.2",
|
|
11
11
|
"packages": [
|
|
12
12
|
{
|
|
13
13
|
"registryType": "npm",
|
|
14
14
|
"identifier": "@tokenlabai/mcp-server",
|
|
15
|
-
"version": "0.2.
|
|
15
|
+
"version": "0.2.2",
|
|
16
16
|
"transport": {
|
|
17
17
|
"type": "stdio"
|
|
18
18
|
},
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
},
|
|
26
26
|
{
|
|
27
27
|
"name": "TOKENLAB_API_KEY",
|
|
28
|
-
"description": "Optional TokenLab API key. Required only for inference tools such as create_response, create_anthropic_message, and create_gemini_content.",
|
|
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
29
|
"isRequired": false,
|
|
30
30
|
"format": "string",
|
|
31
31
|
"isSecret": true
|
package/src/index.js
CHANGED
|
@@ -4,7 +4,7 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
|
4
4
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
5
5
|
import { z } from "zod";
|
|
6
6
|
|
|
7
|
-
const VERSION = "0.2.
|
|
7
|
+
const VERSION = "0.2.2";
|
|
8
8
|
const API_BASE = (process.env.TOKENLAB_API_BASE || "https://api.tokenlab.sh").replace(/\/$/, "");
|
|
9
9
|
const API_KEY = process.env.TOKENLAB_API_KEY || "";
|
|
10
10
|
|
|
@@ -20,6 +20,28 @@ const scenes = [
|
|
|
20
20
|
"translation"
|
|
21
21
|
];
|
|
22
22
|
|
|
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();
|
|
35
|
+
|
|
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();
|
|
44
|
+
|
|
23
45
|
const server = new McpServer({
|
|
24
46
|
name: "tokenlab",
|
|
25
47
|
version: VERSION
|
|
@@ -147,6 +169,60 @@ server.tool(
|
|
|
147
169
|
}
|
|
148
170
|
);
|
|
149
171
|
|
|
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
|
+
);
|
|
217
|
+
|
|
218
|
+
return textResult(await fetchJson("/v1/chat/completions", {
|
|
219
|
+
method: "POST",
|
|
220
|
+
auth: true,
|
|
221
|
+
body
|
|
222
|
+
}));
|
|
223
|
+
}
|
|
224
|
+
);
|
|
225
|
+
|
|
150
226
|
server.tool(
|
|
151
227
|
"create_response",
|
|
152
228
|
"Create a TokenLab Responses API call. Requires TOKENLAB_API_KEY.",
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
import { stat } from "node:fs/promises";
|
|
4
|
+
import test from "node:test";
|
|
5
|
+
|
|
6
|
+
function startServer() {
|
|
7
|
+
const child = spawn(process.execPath, ["src/index.js"], {
|
|
8
|
+
cwd: process.cwd(),
|
|
9
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
10
|
+
});
|
|
11
|
+
let nextId = 1;
|
|
12
|
+
let buffer = "";
|
|
13
|
+
const pending = new Map();
|
|
14
|
+
|
|
15
|
+
child.stdout.setEncoding("utf8");
|
|
16
|
+
child.stdout.on("data", (chunk) => {
|
|
17
|
+
buffer += chunk;
|
|
18
|
+
const lines = buffer.split("\n");
|
|
19
|
+
buffer = lines.pop();
|
|
20
|
+
|
|
21
|
+
for (const line of lines) {
|
|
22
|
+
if (!line.trim()) continue;
|
|
23
|
+
const message = JSON.parse(line);
|
|
24
|
+
const resolve = pending.get(message.id);
|
|
25
|
+
if (resolve) {
|
|
26
|
+
pending.delete(message.id);
|
|
27
|
+
resolve(message);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
function request(method, params) {
|
|
33
|
+
const id = nextId++;
|
|
34
|
+
return new Promise((resolve, reject) => {
|
|
35
|
+
const timeout = setTimeout(() => {
|
|
36
|
+
pending.delete(id);
|
|
37
|
+
reject(new Error(`Timed out waiting for ${method}`));
|
|
38
|
+
}, 5_000);
|
|
39
|
+
pending.set(id, (message) => {
|
|
40
|
+
clearTimeout(timeout);
|
|
41
|
+
resolve(message);
|
|
42
|
+
});
|
|
43
|
+
child.stdin.write(`${JSON.stringify({ jsonrpc: "2.0", id, method, params })}\n`);
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function notify(method, params) {
|
|
48
|
+
child.stdin.write(`${JSON.stringify({ jsonrpc: "2.0", method, params })}\n`);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return { child, notify, request };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
test("advertises an OpenAI-compatible Chat Completions tool", async (t) => {
|
|
55
|
+
const server = startServer();
|
|
56
|
+
t.after(() => server.child.kill());
|
|
57
|
+
|
|
58
|
+
const initialized = await server.request("initialize", {
|
|
59
|
+
protocolVersion: "2025-11-25",
|
|
60
|
+
capabilities: {},
|
|
61
|
+
clientInfo: { name: "tokenlab-mcp-test", version: "0.0.0" }
|
|
62
|
+
});
|
|
63
|
+
assert.equal(initialized.error, undefined);
|
|
64
|
+
|
|
65
|
+
server.notify("notifications/initialized");
|
|
66
|
+
const listed = await server.request("tools/list", {});
|
|
67
|
+
const tool = listed.result.tools.find(({ name }) => name === "create_chat_completion");
|
|
68
|
+
|
|
69
|
+
assert.ok(tool);
|
|
70
|
+
assert.equal(tool.inputSchema.properties.model.type, "string");
|
|
71
|
+
assert.equal(tool.inputSchema.properties.messages.type, "array");
|
|
72
|
+
assert.equal(tool.inputSchema.properties.stream, undefined);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test("ships an executable npm binary", async () => {
|
|
76
|
+
const { mode } = await stat(new URL("../src/index.js", import.meta.url));
|
|
77
|
+
assert.notEqual(mode & 0o111, 0);
|
|
78
|
+
});
|