hubris-mcp 0.1.1 → 0.2.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.
Files changed (4) hide show
  1. package/README.md +131 -118
  2. package/dist/index.js +64 -51
  3. package/dist/tools.js +970 -44
  4. package/package.json +2 -2
package/README.md CHANGED
@@ -1,118 +1,131 @@
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)
1
+ # Hubris MCP
2
+
3
+ [![smithery badge](https://smithery.ai/badge/hubris/hubris-mcp)](https://smithery.ai/servers/hubris/hubris-mcp) [![npm](https://img.shields.io/npm/v/hubris-mcp)](https://www.npmjs.com/package/hubris-mcp)
4
+
5
+ [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 spending, and every kind of model in it: text, embeddings, rerankers, decision models, images, speech, transcription, video and YouTube transcripts.
6
+
7
+ 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.
8
+
9
+ Listing the tools needs no credential; only calling one does. With `HUBRIS_API_KEY` set, the stdio server takes the tool list live from the hosted endpoint, so it never lags behind new tools; without a key it serves the list bundled with the package.
10
+
11
+ - **Endpoint:** `https://api.hubris.pw/mcp`
12
+ - **Transport:** Streamable HTTP
13
+ - **Auth:** Bearer API key (`sk-gw-...`, create one at [hubris.pw/keys](https://hubris.pw/keys))
14
+ - **Full docs:** [hubris.pw/docs/integrations/mcp](https://hubris.pw/docs/integrations/mcp)
15
+
16
+ ## Tools
17
+
18
+ | Tool | Description |
19
+ | --- | --- |
20
+ | `models_list` | Full catalog of active models, cursor-paginated. |
21
+ | `models_search` | Filter by the tool that runs the model (`tool`), capability, price, context length. |
22
+ | `models_get_pricing` | Pricing for a specific model, in rubles; voices for speech models, durations and resolutions for video models. |
23
+ | `balance_get` | Current account balance. |
24
+ | `usage_get` | Spending over a period: requests, tokens, rubles. |
25
+ | `chat_complete` | LLM chat request — same as `POST /v1/chat/completions`. |
26
+ | `embeddings_create` | Embedding vectors — `POST /v1/embeddings`. |
27
+ | `rerank` | Order documents by relevance to a query — `POST /v1/rerank`. |
28
+ | `decisions_ask` | Yes/no, choice and scale answers with probabilities — `POST /v1/decisions`. |
29
+ | `image_generate` | Draw an image, returned as image content — `POST /v1/images/generations`. |
30
+ | `speech_create` | Text to speech, returned as audio content — `POST /v1/audio/speech`. |
31
+ | `transcription_create` | Speech to text from base64 audio — `POST /v1/audio/transcriptions`. |
32
+ | `video_create` | Start a video generation — `POST /v1/videos`. |
33
+ | `video_get` | Status of a video task; download links once it is ready — `GET /v1/videos/{id}`. |
34
+ | `youtube_transcribe` | Transcript or summary of a YouTube video — `POST /v1/video/transcriptions`. |
35
+
36
+ ## Resources
37
+
38
+ | URI | Content |
39
+ | --- | --- |
40
+ | `hubris://catalog/models` | Full model catalog as a single JSON document. |
41
+ | `hubris://docs/quickstart` | Quickstart page. |
42
+ | `hubris://videos/{id}/content` | A finished video, linked from `video_get` (hosted endpoint only). |
43
+
44
+ ## Prompts
45
+
46
+ - `compare-models {task}` — ready-made prompt for comparing models for a given task.
47
+
48
+ ## Run locally (stdio)
49
+
50
+ ```bash
51
+ npm install && npm run build
52
+ HUBRIS_API_KEY=sk-gw-... node dist/index.js
53
+ ```
54
+
55
+ Or with Docker:
56
+
57
+ ```bash
58
+ docker build -t hubris-mcp .
59
+ docker run -i --rm -e HUBRIS_API_KEY=sk-gw-... hubris-mcp
60
+ ```
61
+
62
+ In an MCP client config:
63
+
64
+ ```json
65
+ {
66
+ "mcpServers": {
67
+ "hubris": {
68
+ "command": "node",
69
+ "args": ["/path/to/hubris-mcp/dist/index.js"],
70
+ "env": { "HUBRIS_API_KEY": "sk-gw-..." }
71
+ }
72
+ }
73
+ }
74
+ ```
75
+
76
+ `HUBRIS_MCP_URL` overrides the endpoint if you need to point it elsewhere.
77
+
78
+ ## Connect
79
+
80
+ ### Claude Code
81
+
82
+ ```bash
83
+ claude mcp add --transport http hubris https://api.hubris.pw/mcp \
84
+ --header "Authorization: Bearer sk-gw-..."
85
+ ```
86
+
87
+ ### Claude Desktop
88
+
89
+ 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`:
90
+
91
+ ```json
92
+ {
93
+ "mcpServers": {
94
+ "hubris": {
95
+ "command": "npx",
96
+ "args": [
97
+ "-y",
98
+ "mcp-remote",
99
+ "https://api.hubris.pw/mcp",
100
+ "--transport",
101
+ "http-only",
102
+ "--header",
103
+ "Authorization:${AUTH_TOKEN}"
104
+ ],
105
+ "env": {
106
+ "AUTH_TOKEN": "Bearer sk-gw-..."
107
+ }
108
+ }
109
+ }
110
+ }
111
+ ```
112
+
113
+ ### Cline / Cursor / other clients
114
+
115
+ Any MCP client with Streamable HTTP transport support:
116
+
117
+ | Field | Value |
118
+ | --- | --- |
119
+ | Transport | Streamable HTTP |
120
+ | URL | `https://api.hubris.pw/mcp` |
121
+ | Auth header | `Authorization: Bearer sk-gw-...` |
122
+
123
+ Full connection guide (with client-specific caveats): [hubris.pw/docs/integrations/mcp](https://hubris.pw/docs/integrations/mcp).
124
+
125
+ ## Security
126
+
127
+ 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.
128
+
129
+ ## About Hubris
130
+
131
+ 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 CHANGED
@@ -6,9 +6,11 @@
6
6
  * package is the local end of that: it declares the tool surface and forwards
7
7
  * calls to https://api.hubris.pw/mcp with the caller's key.
8
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.
9
+ * With a key, the tool list comes live from the hosted server, so the package
10
+ * never lags behind new tools. Without one — or if the server cannot be
11
+ * reached — the list bundled with this version is served, so a client or a
12
+ * directory's introspection crawler can read the tools and their schemas
13
+ * without any credential. Only an actual call needs HUBRIS_API_KEY.
12
14
  *
13
15
  * Configuration:
14
16
  * HUBRIS_API_KEY required to call a tool, not to list them
@@ -19,34 +21,20 @@ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
19
21
  import { CallToolRequestSchema, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
20
22
  import { TOOLS } from './tools.js';
21
23
  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
- }
24
+ const PACKAGE_VERSION = '0.2.0';
25
+ 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.
26
+
27
+ The usual order of work:
28
+ 1. models_search — narrow the catalogue by the tool that runs the model, capability, price ceiling and context window. Every model in the answer names its \`tool\`.
29
+ 2. models_get_pricing — confirm what the chosen model actually costs, and for speech and video models which voices or durations it takes.
30
+ 3. Run the request with that tool: chat_complete for text, embeddings_create, rerank, decisions_ask, image_generate, speech_create, transcription_create, video_create (then poll video_get) or youtube_transcribe.
31
+ balance_get shows what is left on the account and usage_get what has been spent; check them before and after a long run.
32
+
33
+ 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".
34
+
35
+ Tools that run a model spend money from the account; the catalogue, balance_get, usage_get and video_get only read.`;
36
+ /** One JSON-RPC request to the hosted server. Throws with a readable reason. */
37
+ async function rpc(apiKey, method, params) {
50
38
  let response;
51
39
  try {
52
40
  response = await fetch(ENDPOINT, {
@@ -56,25 +44,16 @@ async function callRemote(name, args) {
56
44
  'content-type': 'application/json',
57
45
  accept: 'application/json, text/event-stream',
58
46
  },
59
- body: JSON.stringify({
60
- jsonrpc: '2.0',
61
- id: 1,
62
- method: 'tools/call',
63
- params: { name, arguments: args ?? {} },
64
- }),
47
+ body: JSON.stringify({ jsonrpc: '2.0', id: 1, method, params }),
65
48
  });
66
49
  }
67
50
  catch (cause) {
68
51
  const reason = cause instanceof Error ? cause.message : String(cause);
69
- return { isError: true, content: [{ type: 'text', text: `Could not reach ${ENDPOINT}: ${reason}` }] };
52
+ throw new Error(`Could not reach ${ENDPOINT}: ${reason}`);
70
53
  }
71
54
  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
- }
55
+ if (!response.ok)
56
+ throw new Error(`Hubris answered ${response.status}: ${body.slice(0, 600)}`);
78
57
  // Streamable HTTP answers either with plain JSON or with a single SSE frame.
79
58
  const payload = body.trimStart().startsWith('{')
80
59
  ? body
@@ -84,18 +63,52 @@ async function callRemote(name, args) {
84
63
  parsed = JSON.parse(payload);
85
64
  }
86
65
  catch {
87
- return { isError: true, content: [{ type: 'text', text: `Unreadable answer from Hubris: ${body.slice(0, 600)}` }] };
66
+ throw new Error(`Unreadable answer from Hubris: ${body.slice(0, 600)}`);
67
+ }
68
+ if (parsed.error)
69
+ throw new Error(parsed.error.message ?? 'Hubris returned an error.');
70
+ if (parsed.result === undefined)
71
+ throw new Error('Hubris returned no result.');
72
+ return parsed.result;
73
+ }
74
+ /** The live tool list with a key; the bundled one without a key or when the server is unreachable. */
75
+ async function listTools() {
76
+ const apiKey = process.env.HUBRIS_API_KEY;
77
+ if (!apiKey)
78
+ return TOOLS;
79
+ try {
80
+ const { tools } = await rpc(apiKey, 'tools/list', {});
81
+ return tools && tools.length > 0 ? tools : TOOLS;
82
+ }
83
+ catch {
84
+ return TOOLS;
88
85
  }
89
- if (parsed.error) {
90
- return { isError: true, content: [{ type: 'text', text: parsed.error.message ?? 'Hubris returned an error.' }] };
86
+ }
87
+ /** Forwards one tools/call to the hosted server and returns its result. */
88
+ async function callRemote(name, args) {
89
+ const apiKey = process.env.HUBRIS_API_KEY;
90
+ if (!apiKey) {
91
+ return {
92
+ isError: true,
93
+ content: [
94
+ {
95
+ type: 'text',
96
+ text: 'HUBRIS_API_KEY is not set. Create a key at https://hubris.pw/keys and pass it ' +
97
+ 'to this server through the environment. Listing tools works without one; ' +
98
+ 'calling them does not.',
99
+ },
100
+ ],
101
+ };
91
102
  }
92
- if (!parsed.result) {
93
- return { isError: true, content: [{ type: 'text', text: 'Hubris returned no result.' }] };
103
+ try {
104
+ return await rpc(apiKey, 'tools/call', { name, arguments: args ?? {} });
105
+ }
106
+ catch (error) {
107
+ return { isError: true, content: [{ type: 'text', text: error instanceof Error ? error.message : String(error) }] };
94
108
  }
95
- return parsed.result;
96
109
  }
97
110
  const server = new Server({ name: 'hubris', version: PACKAGE_VERSION }, { capabilities: { tools: {} }, instructions: INSTRUCTIONS });
98
- server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }));
111
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: await listTools() }));
99
112
  server.setRequestHandler(CallToolRequestSchema, async (request) => callRemote(request.params.name, request.params.arguments));
100
113
  async function main() {
101
114
  await server.connect(new StdioServerTransport());