hubris-mcp 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Aimagine-life
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,118 @@
1
+ # Hubris MCP
2
+
3
+ [Model Context Protocol](https://modelcontextprotocol.io) server for [Hubris](https://hubris.pw) — an OpenAI-compatible LLM gateway with billing in rubles. Connect Claude Desktop, Claude Code, Cline, Cursor or any other MCP client to a single hosted server and get access to the model catalog, account balance and chat completions.
4
+
5
+ There are two ways in: connect your client straight to the hosted endpoint, or run the stdio server in this repository, which declares the same tools locally and forwards calls to that endpoint. The second is what you want for clients that speak stdio only, and for sandboxes that build from a Dockerfile.
6
+
7
+ Listing the tools needs no credential; only calling one does.
8
+
9
+ - **Endpoint:** `https://api.hubris.pw/mcp`
10
+ - **Transport:** Streamable HTTP
11
+ - **Auth:** Bearer API key (`sk-gw-...`, create one at [hubris.pw/keys](https://hubris.pw/keys))
12
+ - **Full docs:** [hubris.pw/docs/integrations/mcp](https://hubris.pw/docs/integrations/mcp)
13
+
14
+ ## Tools
15
+
16
+ | Tool | Description |
17
+ | --- | --- |
18
+ | `models_list` | Full catalog of active models, cursor-paginated. |
19
+ | `models_search` | Filter by capability (vision / reasoning / tools / web_search / audio_in / audio_out / image_gen / transcription), price, context length. |
20
+ | `models_get_pricing` | Pricing for a specific model, in rubles. |
21
+ | `balance_get` | Current account balance. |
22
+ | `chat_complete` | LLM chat request — full parity with `POST /v1/chat/completions`. |
23
+
24
+ ## Resources
25
+
26
+ | URI | Content |
27
+ | --- | --- |
28
+ | `hubris://catalog/models` | Full model catalog as a single JSON document. |
29
+ | `hubris://docs/quickstart` | Quickstart page. |
30
+
31
+ ## Prompts
32
+
33
+ - `compare-models {task}` — ready-made prompt for comparing models for a given task.
34
+
35
+ ## Run locally (stdio)
36
+
37
+ ```bash
38
+ npm install && npm run build
39
+ HUBRIS_API_KEY=sk-gw-... node dist/index.js
40
+ ```
41
+
42
+ Or with Docker:
43
+
44
+ ```bash
45
+ docker build -t hubris-mcp .
46
+ docker run -i --rm -e HUBRIS_API_KEY=sk-gw-... hubris-mcp
47
+ ```
48
+
49
+ In an MCP client config:
50
+
51
+ ```json
52
+ {
53
+ "mcpServers": {
54
+ "hubris": {
55
+ "command": "node",
56
+ "args": ["/path/to/hubris-mcp/dist/index.js"],
57
+ "env": { "HUBRIS_API_KEY": "sk-gw-..." }
58
+ }
59
+ }
60
+ }
61
+ ```
62
+
63
+ `HUBRIS_MCP_URL` overrides the endpoint if you need to point it elsewhere.
64
+
65
+ ## Connect
66
+
67
+ ### Claude Code
68
+
69
+ ```bash
70
+ claude mcp add --transport http hubris https://api.hubris.pw/mcp \
71
+ --header "Authorization: Bearer sk-gw-..."
72
+ ```
73
+
74
+ ### Claude Desktop
75
+
76
+ Claude Desktop only speaks stdio, so remote HTTP servers go through the [`mcp-remote`](https://github.com/geelen/mcp-remote) shim. Add to `claude_desktop_config.json`:
77
+
78
+ ```json
79
+ {
80
+ "mcpServers": {
81
+ "hubris": {
82
+ "command": "npx",
83
+ "args": [
84
+ "-y",
85
+ "mcp-remote",
86
+ "https://api.hubris.pw/mcp",
87
+ "--transport",
88
+ "http-only",
89
+ "--header",
90
+ "Authorization:${AUTH_TOKEN}"
91
+ ],
92
+ "env": {
93
+ "AUTH_TOKEN": "Bearer sk-gw-..."
94
+ }
95
+ }
96
+ }
97
+ }
98
+ ```
99
+
100
+ ### Cline / Cursor / other clients
101
+
102
+ Any MCP client with Streamable HTTP transport support:
103
+
104
+ | Field | Value |
105
+ | --- | --- |
106
+ | Transport | Streamable HTTP |
107
+ | URL | `https://api.hubris.pw/mcp` |
108
+ | Auth header | `Authorization: Bearer sk-gw-...` |
109
+
110
+ Full connection guide (with client-specific caveats): [hubris.pw/docs/integrations/mcp](https://hubris.pw/docs/integrations/mcp).
111
+
112
+ ## Security
113
+
114
+ The Bearer key grants access to your account balance. We recommend creating a dedicated key for MCP at [hubris.pw/keys](https://hubris.pw/keys) so it can be revoked independently of other integrations.
115
+
116
+ ## About Hubris
117
+
118
+ Hubris is an OpenAI-compatible LLM gateway for the Russian market — one API key, one ruble-denominated balance, 400+ models. [hubris.pw](https://hubris.pw)
package/dist/index.js ADDED
@@ -0,0 +1,106 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Hubris MCP server, stdio transport.
4
+ *
5
+ * Hubris is a hosted service, so the tools themselves run on our side. This
6
+ * package is the local end of that: it declares the tool surface and forwards
7
+ * calls to https://api.hubris.pw/mcp with the caller's key.
8
+ *
9
+ * The declaration is deliberately local rather than proxied. A client — or a
10
+ * directory's introspection crawler — can list the tools and read their
11
+ * schemas without any credential; only an actual call needs HUBRIS_API_KEY.
12
+ *
13
+ * Configuration:
14
+ * HUBRIS_API_KEY required to call a tool, not to list them
15
+ * HUBRIS_MCP_URL override the endpoint (defaults to the hosted one)
16
+ */
17
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
18
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
19
+ import { CallToolRequestSchema, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
20
+ import { TOOLS } from './tools.js';
21
+ const ENDPOINT = process.env.HUBRIS_MCP_URL ?? 'https://api.hubris.pw/mcp';
22
+ const PACKAGE_VERSION = '0.1.0';
23
+ const INSTRUCTIONS = `Hubris is an OpenAI-compatible LLM gateway for the Russian market. It puts around 550 models from every major vendor behind one API billed in rubles, and this server exposes that catalogue, the account balance and the models themselves.
24
+
25
+ The usual order of work:
26
+ 1. models_search — narrow the catalogue by capability, price ceiling and context window.
27
+ 2. models_get_pricing — confirm what the chosen model actually costs.
28
+ 3. chat_complete — run the request on it.
29
+ balance_get shows what is left on the account; check it before a long run.
30
+
31
+ Two things to keep in mind. Model ids are matched exactly and always carry the vendor prefix, as in "anthropic/claude-sonnet-5" — short names and names with a trailing date are rejected. And a "0.00" token price does not mean free: models billed per image, per second or per thousand characters report zero token rates, and their real price is in models_get_pricing under perUnit. The isFree flag is the only reliable answer to "is this free".
32
+
33
+ Everything except chat_complete is read-only. chat_complete spends money from the account.`;
34
+ /** Forwards one tools/call to the hosted server and returns its result. */
35
+ async function callRemote(name, args) {
36
+ const apiKey = process.env.HUBRIS_API_KEY;
37
+ if (!apiKey) {
38
+ return {
39
+ isError: true,
40
+ content: [
41
+ {
42
+ type: 'text',
43
+ text: 'HUBRIS_API_KEY is not set. Create a key at https://hubris.pw/keys and pass it ' +
44
+ 'to this server through the environment. Listing tools works without one; ' +
45
+ 'calling them does not.',
46
+ },
47
+ ],
48
+ };
49
+ }
50
+ let response;
51
+ try {
52
+ response = await fetch(ENDPOINT, {
53
+ method: 'POST',
54
+ headers: {
55
+ authorization: `Bearer ${apiKey}`,
56
+ 'content-type': 'application/json',
57
+ accept: 'application/json, text/event-stream',
58
+ },
59
+ body: JSON.stringify({
60
+ jsonrpc: '2.0',
61
+ id: 1,
62
+ method: 'tools/call',
63
+ params: { name, arguments: args ?? {} },
64
+ }),
65
+ });
66
+ }
67
+ catch (cause) {
68
+ const reason = cause instanceof Error ? cause.message : String(cause);
69
+ return { isError: true, content: [{ type: 'text', text: `Could not reach ${ENDPOINT}: ${reason}` }] };
70
+ }
71
+ const body = await response.text();
72
+ if (!response.ok) {
73
+ return {
74
+ isError: true,
75
+ content: [{ type: 'text', text: `Hubris answered ${response.status}: ${body.slice(0, 600)}` }],
76
+ };
77
+ }
78
+ // Streamable HTTP answers either with plain JSON or with a single SSE frame.
79
+ const payload = body.trimStart().startsWith('{')
80
+ ? body
81
+ : (body.split('\n').find((line) => line.startsWith('data: '))?.slice('data: '.length) ?? '{}');
82
+ let parsed;
83
+ try {
84
+ parsed = JSON.parse(payload);
85
+ }
86
+ catch {
87
+ return { isError: true, content: [{ type: 'text', text: `Unreadable answer from Hubris: ${body.slice(0, 600)}` }] };
88
+ }
89
+ if (parsed.error) {
90
+ return { isError: true, content: [{ type: 'text', text: parsed.error.message ?? 'Hubris returned an error.' }] };
91
+ }
92
+ if (!parsed.result) {
93
+ return { isError: true, content: [{ type: 'text', text: 'Hubris returned no result.' }] };
94
+ }
95
+ return parsed.result;
96
+ }
97
+ const server = new Server({ name: 'hubris', version: PACKAGE_VERSION }, { capabilities: { tools: {} }, instructions: INSTRUCTIONS });
98
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }));
99
+ server.setRequestHandler(CallToolRequestSchema, async (request) => callRemote(request.params.name, request.params.arguments));
100
+ async function main() {
101
+ await server.connect(new StdioServerTransport());
102
+ }
103
+ main().catch((error) => {
104
+ console.error('hubris-mcp failed to start:', error);
105
+ process.exit(1);
106
+ });
package/dist/tools.js ADDED
@@ -0,0 +1,510 @@
1
+ export const TOOLS = [
2
+ {
3
+ "name": "models_list",
4
+ "title": "List models",
5
+ "description": "Walk the whole Hubris catalogue page by page, ordered by model id. Returns every active model with its capabilities, context window and ruble price.\n\nUse this when you need the complete catalogue — to cache it, to count models, or to scan for something no filter covers. When you are looking for a model that fits a task, use models_search instead: it filters by capability, price and context server-side and saves you a few hundred rows.\n\nPaging: read `nextCursor` from the response and pass it back as `cursor`. When `nextCursor` is absent you have reached the end. The catalogue holds roughly 550 models, so a full walk is about 11 calls at the default page size.",
6
+ "inputSchema": {
7
+ "type": "object",
8
+ "properties": {
9
+ "cursor": {
10
+ "type": "string",
11
+ "description": "The `nextCursor` value from the previous response. Omit for the first page."
12
+ },
13
+ "limit": {
14
+ "type": "integer",
15
+ "minimum": 1,
16
+ "maximum": 200,
17
+ "default": 50,
18
+ "description": "Models per page, 1–200. Defaults to 50."
19
+ }
20
+ },
21
+ "additionalProperties": false,
22
+ "$schema": "http://json-schema.org/draft-07/schema#"
23
+ },
24
+ "annotations": {
25
+ "readOnlyHint": true,
26
+ "destructiveHint": false,
27
+ "idempotentHint": true,
28
+ "openWorldHint": false
29
+ },
30
+ "execution": {
31
+ "taskSupport": "forbidden"
32
+ },
33
+ "outputSchema": {
34
+ "type": "object",
35
+ "properties": {
36
+ "models": {
37
+ "type": "array",
38
+ "items": {
39
+ "type": "object",
40
+ "properties": {
41
+ "id": {
42
+ "type": "string",
43
+ "description": "Model id to pass to chat_complete and to the OpenAI-compatible API, e.g. \"anthropic/claude-sonnet-5\"."
44
+ },
45
+ "displayName": {
46
+ "type": "string",
47
+ "description": "Human-readable model name."
48
+ },
49
+ "capabilities": {
50
+ "type": "array",
51
+ "items": {
52
+ "type": "string"
53
+ },
54
+ "description": "Any of: vision, image_gen, transcription, audio_in, audio_out, audio (legacy, means either audio direction), tools, reasoning, web_search."
55
+ },
56
+ "contextLength": {
57
+ "type": "number",
58
+ "description": "Context window in tokens."
59
+ },
60
+ "pricing": {
61
+ "type": "object",
62
+ "properties": {
63
+ "unit": {
64
+ "type": "string",
65
+ "enum": [
66
+ "token",
67
+ "unit",
68
+ "unknown"
69
+ ],
70
+ "description": "Billing unit. \"token\" — priced per token, the two Rub fields below are meaningful. \"unit\" — priced per image, per minute, per character and so on; token fields are \"0.00\" and the real price is in models_get_pricing → perUnit. \"unknown\" — no pricing on record."
71
+ },
72
+ "inputPer1MRub": {
73
+ "type": "string",
74
+ "description": "Price of 1M input tokens in rubles, decimal string, e.g. \"182.40\". \"0.00\" when unit is not \"token\"."
75
+ },
76
+ "outputPer1MRub": {
77
+ "type": "string",
78
+ "description": "Price of 1M output tokens in rubles, decimal string. \"0.00\" when unit is not \"token\"."
79
+ },
80
+ "isFree": {
81
+ "type": "boolean",
82
+ "description": "True only for genuinely free models. Do not infer this from a zero price: models billed per image or per minute also report \"0.00\" token rates."
83
+ }
84
+ },
85
+ "required": [
86
+ "unit",
87
+ "inputPer1MRub",
88
+ "outputPer1MRub",
89
+ "isFree"
90
+ ],
91
+ "additionalProperties": false
92
+ }
93
+ },
94
+ "required": [
95
+ "id",
96
+ "displayName",
97
+ "capabilities",
98
+ "contextLength",
99
+ "pricing"
100
+ ],
101
+ "additionalProperties": false
102
+ }
103
+ },
104
+ "nextCursor": {
105
+ "type": "string",
106
+ "description": "Pass back as `cursor` to fetch the next page. Absent when the last page was returned."
107
+ }
108
+ },
109
+ "required": [
110
+ "models"
111
+ ],
112
+ "additionalProperties": false,
113
+ "$schema": "http://json-schema.org/draft-07/schema#"
114
+ }
115
+ },
116
+ {
117
+ "name": "models_search",
118
+ "title": "Search models",
119
+ "description": "Find models that fit a task. Filters by name, capability, price ceiling and context window, and can sort the result. This is the tool to reach for when choosing a model — it does the filtering server-side and returns at most 50 rows.\n\nAll filters combine with AND, and every one of them is optional: calling with no arguments returns the first 50 active models.\n\nTwo things worth knowing about prices. `maxInputPer1MRub` keeps only models billed per token, because a ruble-per-million ceiling is meaningless for a model billed per image. The `cheap_input` and `cheap_output` sorts push non-token models to the end of the list for the same reason — their token rate reads as zero, which would otherwise put video models at the top of \"cheapest\".",
120
+ "inputSchema": {
121
+ "type": "object",
122
+ "properties": {
123
+ "query": {
124
+ "type": "string",
125
+ "description": "Case-insensitive substring matched against the model id and display name, e.g. \"claude\" or \"gpt\"."
126
+ },
127
+ "capability": {
128
+ "type": "string",
129
+ "enum": [
130
+ "vision",
131
+ "reasoning",
132
+ "tools",
133
+ "web_search",
134
+ "audio_in",
135
+ "audio_out",
136
+ "audio",
137
+ "image_gen",
138
+ "transcription"
139
+ ],
140
+ "description": "Keep only models with this capability. vision — reads images. image_gen — draws them. transcription — speech to text via /v1/audio/transcriptions. audio_in — accepts an audio recording in chat. audio_out — answers with speech. audio — legacy, matches either audio direction. tools — function calling. reasoning — extended thinking. web_search — built-in web search."
141
+ },
142
+ "maxInputPer1MRub": {
143
+ "type": "number",
144
+ "description": "Ceiling on the price of 1M input tokens, in rubles. Also restricts the result to models billed per token, since the ceiling means nothing for models billed per image or minute."
145
+ },
146
+ "minContext": {
147
+ "type": "integer",
148
+ "description": "Keep only models whose context window is at least this many tokens."
149
+ },
150
+ "sort": {
151
+ "type": "string",
152
+ "enum": [
153
+ "cheap_input",
154
+ "cheap_output",
155
+ "big_context",
156
+ "popular"
157
+ ],
158
+ "description": "cheap_input / cheap_output — ascending token price, models not billed per token go last. big_context — widest context window first. popular — most used on Hubris first. Unsorted when omitted."
159
+ }
160
+ },
161
+ "additionalProperties": false,
162
+ "$schema": "http://json-schema.org/draft-07/schema#"
163
+ },
164
+ "annotations": {
165
+ "readOnlyHint": true,
166
+ "destructiveHint": false,
167
+ "idempotentHint": true,
168
+ "openWorldHint": false
169
+ },
170
+ "execution": {
171
+ "taskSupport": "forbidden"
172
+ },
173
+ "outputSchema": {
174
+ "type": "object",
175
+ "properties": {
176
+ "models": {
177
+ "type": "array",
178
+ "items": {
179
+ "type": "object",
180
+ "properties": {
181
+ "id": {
182
+ "type": "string",
183
+ "description": "Model id to pass to chat_complete and to the OpenAI-compatible API, e.g. \"anthropic/claude-sonnet-5\"."
184
+ },
185
+ "displayName": {
186
+ "type": "string",
187
+ "description": "Human-readable model name."
188
+ },
189
+ "capabilities": {
190
+ "type": "array",
191
+ "items": {
192
+ "type": "string"
193
+ },
194
+ "description": "Any of: vision, image_gen, transcription, audio_in, audio_out, audio (legacy, means either audio direction), tools, reasoning, web_search."
195
+ },
196
+ "contextLength": {
197
+ "type": "number",
198
+ "description": "Context window in tokens."
199
+ },
200
+ "pricing": {
201
+ "type": "object",
202
+ "properties": {
203
+ "unit": {
204
+ "type": "string",
205
+ "enum": [
206
+ "token",
207
+ "unit",
208
+ "unknown"
209
+ ],
210
+ "description": "Billing unit. \"token\" — priced per token, the two Rub fields below are meaningful. \"unit\" — priced per image, per minute, per character and so on; token fields are \"0.00\" and the real price is in models_get_pricing → perUnit. \"unknown\" — no pricing on record."
211
+ },
212
+ "inputPer1MRub": {
213
+ "type": "string",
214
+ "description": "Price of 1M input tokens in rubles, decimal string, e.g. \"182.40\". \"0.00\" when unit is not \"token\"."
215
+ },
216
+ "outputPer1MRub": {
217
+ "type": "string",
218
+ "description": "Price of 1M output tokens in rubles, decimal string. \"0.00\" when unit is not \"token\"."
219
+ },
220
+ "isFree": {
221
+ "type": "boolean",
222
+ "description": "True only for genuinely free models. Do not infer this from a zero price: models billed per image or per minute also report \"0.00\" token rates."
223
+ }
224
+ },
225
+ "required": [
226
+ "unit",
227
+ "inputPer1MRub",
228
+ "outputPer1MRub",
229
+ "isFree"
230
+ ],
231
+ "additionalProperties": false
232
+ }
233
+ },
234
+ "required": [
235
+ "id",
236
+ "displayName",
237
+ "capabilities",
238
+ "contextLength",
239
+ "pricing"
240
+ ],
241
+ "additionalProperties": false
242
+ },
243
+ "description": "Matching models, at most 50."
244
+ }
245
+ },
246
+ "required": [
247
+ "models"
248
+ ],
249
+ "additionalProperties": false,
250
+ "$schema": "http://json-schema.org/draft-07/schema#"
251
+ }
252
+ },
253
+ {
254
+ "name": "models_get_pricing",
255
+ "title": "Get model pricing",
256
+ "description": "The full price of one model in rubles — the authoritative answer to \"what does this cost\".\n\nPrefer this over the pricing embedded in models_list and models_search whenever the model is not billed per token. Those two return \"0.00\" token rates for such models; only this tool returns `perUnit`, where the real price per image, per second or per thousand characters lives.\n\nErrors with InvalidParams if the model id is unknown or the model is no longer active.",
257
+ "inputSchema": {
258
+ "type": "object",
259
+ "properties": {
260
+ "modelId": {
261
+ "type": "string",
262
+ "description": "Exact model id from the catalogue, vendor included, e.g. \"openai/gpt-6-astra\"."
263
+ }
264
+ },
265
+ "required": [
266
+ "modelId"
267
+ ],
268
+ "additionalProperties": false,
269
+ "$schema": "http://json-schema.org/draft-07/schema#"
270
+ },
271
+ "annotations": {
272
+ "readOnlyHint": true,
273
+ "destructiveHint": false,
274
+ "idempotentHint": true,
275
+ "openWorldHint": false
276
+ },
277
+ "execution": {
278
+ "taskSupport": "forbidden"
279
+ },
280
+ "outputSchema": {
281
+ "type": "object",
282
+ "properties": {
283
+ "unit": {
284
+ "type": "string",
285
+ "enum": [
286
+ "token",
287
+ "unit",
288
+ "unknown"
289
+ ],
290
+ "description": "Billing unit. \"token\" — priced per token, the two Rub fields below are meaningful. \"unit\" — priced per image, per minute, per character and so on; token fields are \"0.00\" and the real price is in models_get_pricing → perUnit. \"unknown\" — no pricing on record."
291
+ },
292
+ "inputPer1MRub": {
293
+ "type": "string",
294
+ "description": "Price of 1M input tokens in rubles, decimal string, e.g. \"182.40\". \"0.00\" when unit is not \"token\"."
295
+ },
296
+ "outputPer1MRub": {
297
+ "type": "string",
298
+ "description": "Price of 1M output tokens in rubles, decimal string. \"0.00\" when unit is not \"token\"."
299
+ },
300
+ "isFree": {
301
+ "type": "boolean",
302
+ "description": "True only for genuinely free models. Do not infer this from a zero price: models billed per image or per minute also report \"0.00\" token rates."
303
+ },
304
+ "currency": {
305
+ "type": "string",
306
+ "const": "RUB",
307
+ "description": "All prices are in rubles."
308
+ },
309
+ "perUnit": {
310
+ "type": "array",
311
+ "items": {
312
+ "type": "object",
313
+ "properties": {
314
+ "sku": {
315
+ "type": "string",
316
+ "description": "What exactly is billed, e.g. \"image\", \"video\", \"speech\"."
317
+ },
318
+ "unit": {
319
+ "type": "string",
320
+ "description": "Unit of billing, e.g. \"image\", \"second\", \"1k characters\"."
321
+ },
322
+ "priceRub": {
323
+ "type": "string",
324
+ "description": "Price of one unit in rubles, decimal string."
325
+ }
326
+ },
327
+ "required": [
328
+ "sku",
329
+ "unit",
330
+ "priceRub"
331
+ ],
332
+ "additionalProperties": false
333
+ },
334
+ "description": "Present only for models not billed per token. This is where their real price lives."
335
+ }
336
+ },
337
+ "required": [
338
+ "unit",
339
+ "inputPer1MRub",
340
+ "outputPer1MRub",
341
+ "isFree",
342
+ "currency"
343
+ ],
344
+ "additionalProperties": false,
345
+ "$schema": "http://json-schema.org/draft-07/schema#"
346
+ }
347
+ },
348
+ {
349
+ "name": "balance_get",
350
+ "title": "Get account balance",
351
+ "description": "Current balance of the Hubris account the API key belongs to, in rubles.\n\nWorth checking before a long or expensive run: requests are refused once the balance is spent. The balance can go slightly negative — one operation is allowed to overdraw it, and the next one is refused.\n\nTwo representations of the same number come back. Use `balanceKopecks` for arithmetic and comparisons — it is an exact integer string. Use `balanceRub` for display.",
352
+ "inputSchema": {
353
+ "$schema": "http://json-schema.org/draft-07/schema#",
354
+ "type": "object",
355
+ "properties": {}
356
+ },
357
+ "annotations": {
358
+ "readOnlyHint": true,
359
+ "destructiveHint": false,
360
+ "idempotentHint": true,
361
+ "openWorldHint": false
362
+ },
363
+ "execution": {
364
+ "taskSupport": "forbidden"
365
+ },
366
+ "outputSchema": {
367
+ "type": "object",
368
+ "properties": {
369
+ "balanceRub": {
370
+ "type": "string",
371
+ "description": "Balance in rubles as a decimal string, e.g. \"1240.55\". Negative when the account is overdrawn."
372
+ },
373
+ "balanceKopecks": {
374
+ "type": "string",
375
+ "description": "The same balance in kopecks as an integer string. Use this one for arithmetic: it is exact, while the ruble string is for display."
376
+ }
377
+ },
378
+ "required": [
379
+ "balanceRub",
380
+ "balanceKopecks"
381
+ ],
382
+ "additionalProperties": false,
383
+ "$schema": "http://json-schema.org/draft-07/schema#"
384
+ }
385
+ },
386
+ {
387
+ "name": "chat_complete",
388
+ "title": "Chat completion",
389
+ "description": "Run a chat completion on any model in the Hubris catalogue and get the answer back. Full parity with POST /v1/chat/completions — same request shape, same response shape, including tool calls, structured output and reasoning. Streaming is not available here: MCP tool calls are synchronous, and `stream` is ignored if you pass it.\n\nThis is how you reach a model you cannot reach otherwise — pick an id with models_search, then call it here. Useful for delegating a subtask to a cheaper or a stronger model than the one you are running on.\n\nThe call spends real money from the account balance: it is billed per token at the catalogue price, in rubles. balance_get shows what is left.",
390
+ "inputSchema": {
391
+ "type": "object",
392
+ "properties": {
393
+ "model": {
394
+ "type": "string",
395
+ "description": "Model id from the catalogue, vendor included and matched exactly, e.g. \"anthropic/claude-sonnet-5\". Short names and names with a trailing date are rejected. Use models_search to find one."
396
+ },
397
+ "messages": {
398
+ "type": "array",
399
+ "items": {
400
+ "type": "object",
401
+ "properties": {
402
+ "role": {
403
+ "type": "string",
404
+ "enum": [
405
+ "system",
406
+ "user",
407
+ "assistant",
408
+ "tool"
409
+ ],
410
+ "description": "Who the message is from."
411
+ },
412
+ "content": {
413
+ "anyOf": [
414
+ {
415
+ "type": "string"
416
+ },
417
+ {
418
+ "type": "array"
419
+ },
420
+ {
421
+ "type": "null"
422
+ }
423
+ ],
424
+ "description": "Message text. Can be an array of parts for multimodal input — {type:\"text\"} and {type:\"image_url\"} — on models whose capabilities include \"vision\"."
425
+ },
426
+ "name": {
427
+ "type": "string",
428
+ "description": "Optional author name."
429
+ },
430
+ "tool_calls": {
431
+ "type": "array",
432
+ "description": "Tool calls the model asked for, on an assistant message."
433
+ },
434
+ "tool_call_id": {
435
+ "type": "string",
436
+ "description": "Which tool call this result answers, on a tool message."
437
+ }
438
+ },
439
+ "required": [
440
+ "role"
441
+ ],
442
+ "additionalProperties": false
443
+ },
444
+ "minItems": 1,
445
+ "description": "Conversation so far, oldest first."
446
+ },
447
+ "temperature": {
448
+ "type": "number",
449
+ "minimum": 0,
450
+ "maximum": 2,
451
+ "description": "Sampling temperature, 0–2. Some newer models reject it; leave it out unless you need it."
452
+ },
453
+ "max_tokens": {
454
+ "type": "integer",
455
+ "minimum": 1,
456
+ "description": "Cap on the length of the answer, in tokens."
457
+ },
458
+ "tools": {
459
+ "type": "array",
460
+ "description": "Function-calling tools, OpenAI shape. Model must have the \"tools\" capability."
461
+ },
462
+ "tool_choice": {
463
+ "description": "\"auto\", \"none\", \"required\", or a specific function to force."
464
+ },
465
+ "response_format": {
466
+ "description": "{type:\"json_object\"} or {type:\"json_schema\", json_schema:{…}} to constrain the answer to JSON."
467
+ },
468
+ "web_search": {
469
+ "description": "Enables built-in web search on models whose capabilities include \"web_search\"."
470
+ },
471
+ "reasoning": {
472
+ "description": "Extended thinking on models with the \"reasoning\" capability: {effort:\"low\"|\"medium\"|\"high\"}, {max_tokens:N}, or {enabled:false} to turn it off."
473
+ },
474
+ "stop": {
475
+ "anyOf": [
476
+ {
477
+ "type": "string"
478
+ },
479
+ {
480
+ "type": "array",
481
+ "items": {
482
+ "type": "string"
483
+ }
484
+ }
485
+ ],
486
+ "description": "Up to four strings that cut the answer short."
487
+ },
488
+ "seed": {
489
+ "type": "integer",
490
+ "description": "Best-effort determinism. Not every model honours it."
491
+ }
492
+ },
493
+ "required": [
494
+ "model",
495
+ "messages"
496
+ ],
497
+ "additionalProperties": false,
498
+ "$schema": "http://json-schema.org/draft-07/schema#"
499
+ },
500
+ "annotations": {
501
+ "readOnlyHint": false,
502
+ "destructiveHint": false,
503
+ "idempotentHint": false,
504
+ "openWorldHint": true
505
+ },
506
+ "execution": {
507
+ "taskSupport": "forbidden"
508
+ }
509
+ }
510
+ ];
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "hubris-mcp",
3
+ "version": "0.1.0",
4
+ "mcpName": "pw.hubris/hubris",
5
+ "description": "MCP server for Hubris — an OpenAI-compatible LLM gateway billed in rubles. Catalogue, balance and chat completions over stdio.",
6
+ "license": "MIT",
7
+ "type": "module",
8
+ "bin": {
9
+ "hubris-mcp": "dist/index.js"
10
+ },
11
+ "main": "dist/index.js",
12
+ "files": [
13
+ "dist",
14
+ "README.md",
15
+ "LICENSE"
16
+ ],
17
+ "engines": {
18
+ "node": ">=20"
19
+ },
20
+ "scripts": {
21
+ "build": "tsc",
22
+ "start": "node dist/index.js",
23
+ "typecheck": "tsc --noEmit"
24
+ },
25
+ "keywords": [
26
+ "mcp",
27
+ "modelcontextprotocol",
28
+ "llm",
29
+ "gateway",
30
+ "hubris"
31
+ ],
32
+ "repository": {
33
+ "type": "git",
34
+ "url": "git+https://github.com/Aimagine-life/hubris-mcp.git"
35
+ },
36
+ "homepage": "https://hubris.pw",
37
+ "dependencies": {
38
+ "@modelcontextprotocol/sdk": "^1.29.0"
39
+ },
40
+ "devDependencies": {
41
+ "typescript": "^5.6.0",
42
+ "@types/node": "^22.0.0"
43
+ }
44
+ }