hubris-mcp 0.1.0 → 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.
- package/README.md +131 -118
- package/dist/index.js +64 -51
- package/dist/tools.js +970 -44
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -1,118 +1,131 @@
|
|
|
1
|
-
# Hubris MCP
|
|
2
|
-
|
|
3
|
-
[
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
- **
|
|
12
|
-
- **
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
|
19
|
-
|
|
|
20
|
-
| `
|
|
21
|
-
| `
|
|
22
|
-
| `
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
|
27
|
-
|
|
|
28
|
-
| `
|
|
29
|
-
| `
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
1
|
+
# Hubris MCP
|
|
2
|
+
|
|
3
|
+
[](https://smithery.ai/servers/hubris/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
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
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.
|
|
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
|
|
29
|
-
balance_get shows what is left on the account; check
|
|
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
|
-
|
|
34
|
-
/**
|
|
35
|
-
async function
|
|
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
|
-
|
|
52
|
+
throw new Error(`Could not reach ${ENDPOINT}: ${reason}`);
|
|
70
53
|
}
|
|
71
54
|
const body = await response.text();
|
|
72
|
-
if (!response.ok)
|
|
73
|
-
|
|
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
|
-
|
|
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
|
-
|
|
90
|
-
|
|
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
|
-
|
|
93
|
-
return
|
|
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:
|
|
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());
|