@reneturcios/modelrelay 0.0.0-stage → 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 +21 -0
- package/README.md +81 -2
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +79 -0
- package/dist/cli.js.map +1 -0
- package/dist/forward.d.ts +85 -0
- package/dist/forward.js +360 -0
- package/dist/forward.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp.d.ts +33 -0
- package/dist/mcp.js +58 -0
- package/dist/mcp.js.map +1 -0
- package/dist/types.d.ts +54 -0
- package/dist/types.js +13 -0
- package/dist/types.js.map +1 -0
- package/package.json +39 -4
- package/server.json +52 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Rene Turcios
|
|
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
CHANGED
|
@@ -1,3 +1,82 @@
|
|
|
1
|
-
#
|
|
1
|
+
# modelrelay
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Open AI models are spread across providers with incompatible APIs and price lists. modelrelay is a small local bridge to the hosted Synap MCP service: you can run a chat completion on one of the open models Synap serves, browse the live catalogue with per-million-token prices, pull the full record for a single model, and estimate what a call will cost before you spend anything.
|
|
4
|
+
|
|
5
|
+
Nothing is stored or computed on your own machine. This package answers `tools/list` locally from the captured tool definitions and forwards every `tools/call` to the hosted service over a streamable-HTTP JSON-RPC session, returning whatever the service sends back unchanged.
|
|
6
|
+
|
|
7
|
+
## The tools
|
|
8
|
+
|
|
9
|
+
Three of the four tools are free catalogue and price reads. The fourth, `synap_chat_completion`, is the only one that costs money: it bills prompt and completion tokens to the Synap API key supplied with the call, against your own pre-existing Synap account. No tool buys credit, tops up a balance, starts a subscription, or creates an API key.
|
|
10
|
+
|
|
11
|
+
- **`synap_chat_completion`** — Sends a conversation to a model and returns the standard OpenAI `chat.completion` JSON: the reply sits in `choices[0].message.content` and token counts in `usage`. The call is not streamed and keeps no memory between calls, so send the full message history each time. The model id `synap-v1` lets Synap pick the cheapest model that can handle the request. `temperature` (0 to 2) and `max_tokens` are optional.
|
|
12
|
+
- **`synap_list_models`** — The live list of models Synap serves right now, each with its price per million tokens, context window and what it is good at. An optional `query` filters case-insensitively against model id, name and capability. Auto-routing ids such as `synap-v1` carry no fixed price; they bill at the price of the model they route to.
|
|
13
|
+
- **`synap_get_model`** — The complete catalogue record for one model: pricing, context and output limits, supported parameters, quantisation, data-retention policy and readiness. An unknown id comes back as an error naming the id.
|
|
14
|
+
- **`synap_estimate_cost`** — Works out what a chat completion would cost from token counts, using the live price list: prompt and completion dollars and the total. `completion_tokens` defaults to 0; use your `max_tokens` for a worst case. Auto-routing ids have no fixed price and return an error saying so.
|
|
15
|
+
|
|
16
|
+
## Install and run
|
|
17
|
+
|
|
18
|
+
As an MCP server via npm:
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
npx -y @reneturcios/modelrelay mcp
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
MCP client configuration:
|
|
25
|
+
|
|
26
|
+
```json
|
|
27
|
+
{
|
|
28
|
+
"mcpServers": {
|
|
29
|
+
"modelrelay": {
|
|
30
|
+
"command": "npx",
|
|
31
|
+
"args": ["-y", "@reneturcios/modelrelay", "mcp"]
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
As a container:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
docker run -i ghcr.io/cloudcorprecords/modelrelay
|
|
41
|
+
docker run --rm ghcr.io/cloudcorprecords/modelrelay synap_get_model '{"model":"qwen3-coder-30b"}'
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## CLI
|
|
45
|
+
|
|
46
|
+
One binary serves both the command line and the MCP server. Each tool is a subcommand taking a single JSON string with that tool's arguments:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
modelrelay synap_list_models '{"query":"coder"}'
|
|
50
|
+
modelrelay synap_chat_completion '{"model":"synap-v1","messages":[{"role":"user","content":"hello"}]}'
|
|
51
|
+
modelrelay synap_get_model '{"model":"qwen3-coder-30b"}'
|
|
52
|
+
modelrelay synap_estimate_cost '{"model":"qwen3-coder-30b","prompt_tokens":500,"completion_tokens":200}'
|
|
53
|
+
modelrelay --version
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Exit codes: `0` when the forwarded call succeeds, `1` when the upstream result is an error, and `2` for usage problems such as an unknown subcommand or a malformed JSON argument.
|
|
57
|
+
|
|
58
|
+
## Configuration
|
|
59
|
+
|
|
60
|
+
- `SYNAP_MCP_URL` — Overrides the upstream endpoint. The default is the pinned hosted endpoint, `https://pool.linkrra.com/mcp`. (The override variable is a reconstruction added by this rebuild.)
|
|
61
|
+
- `SYNAP_API_KEY` — When set, its value is sent to the upstream on every request as an `Authorization: Bearer <value>` header. When it is not set, no key header is sent at all, matching the keyless behaviour the tool surface was captured with. (The hosted service's registry record pins a secret `Authorization` header on its remote; the environment-variable forwarding is a reconstruction added by this rebuild.) Without a key, `synap_chat_completion` returns an error and nothing is spent; the three catalogue tools need no key.
|
|
62
|
+
|
|
63
|
+
Upstream failures — a JSON-RPC error from the service, a non-2xx response, an unreadable body, or a request that fails outright — are reported to the caller as an error result whose text starts with `modelrelay error:`.
|
|
64
|
+
|
|
65
|
+
## How forwarding works
|
|
66
|
+
|
|
67
|
+
The first forwarded call opens a session: an `initialize` request (protocol version `2025-03-26`), followed by a `notifications/initialized` notification, then the `tools/call` itself. If the service answers the initialize request with an `Mcp-Session-Id` header, that value is carried on every later request, and the open session is reused for the rest of the process. Replies are accepted either as plain JSON or as server-sent events. Successful results pass through verbatim, including any `isError` flag and any extra fields the service includes.
|
|
68
|
+
|
|
69
|
+
## Development
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
npm install
|
|
73
|
+
npm run build # tsc -> dist/ (dist/cli.js is the bin)
|
|
74
|
+
npm test # tsx --test test/*.test.ts — stubbed upstream, no network
|
|
75
|
+
npm run dev # run the CLI from source
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
The test suite checks the served tool list against the captured surface tool by tool, exercises the session handshake and its reuse, covers both key-header branches, and verifies that arguments go upstream untouched and results come back untouched.
|
|
79
|
+
|
|
80
|
+
## License
|
|
81
|
+
|
|
82
|
+
MIT — see `LICENSE`. Copyright (c) 2026 Rene Turcios.
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* modelrelay CLI.
|
|
4
|
+
*
|
|
5
|
+
* modelrelay <tool> '<json-args>'
|
|
6
|
+
* modelrelay mcp
|
|
7
|
+
* modelrelay --version
|
|
8
|
+
*
|
|
9
|
+
* <tool> is one of the 4 forwarded tools. Each tool
|
|
10
|
+
* subcommand takes a single JSON argument string holding
|
|
11
|
+
* that tool's arguments, forwards the call the same way
|
|
12
|
+
* the MCP path does, and prints the forwarded result
|
|
13
|
+
* content. Exit codes: 0 success, 1 on an isError
|
|
14
|
+
* result, 2 usage error.
|
|
15
|
+
*/
|
|
16
|
+
import { callTool, TOOL_NAMES, VERSION } from "./forward.js";
|
|
17
|
+
function usage() {
|
|
18
|
+
console.error(`Usage:
|
|
19
|
+
modelrelay <tool> '<json-args>' one of: ${TOOL_NAMES.join(", ")}
|
|
20
|
+
modelrelay mcp
|
|
21
|
+
modelrelay --version`);
|
|
22
|
+
process.exit(2);
|
|
23
|
+
}
|
|
24
|
+
function firstText(result) {
|
|
25
|
+
const content = result.content;
|
|
26
|
+
if (Array.isArray(content)) {
|
|
27
|
+
for (const item of content) {
|
|
28
|
+
if (item !== null &&
|
|
29
|
+
typeof item === "object" &&
|
|
30
|
+
item.type === "text" &&
|
|
31
|
+
typeof item.text === "string") {
|
|
32
|
+
return item.text;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return typeof content === "string" ? content : JSON.stringify(content);
|
|
37
|
+
}
|
|
38
|
+
async function main() {
|
|
39
|
+
const [cmd, ...rest] = process.argv.slice(2);
|
|
40
|
+
if (cmd === "--version" || cmd === "-v") {
|
|
41
|
+
console.log(VERSION);
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
if (cmd === "mcp") {
|
|
45
|
+
if (rest.length !== 0)
|
|
46
|
+
usage();
|
|
47
|
+
const { startMcpServer } = await import("./mcp.js");
|
|
48
|
+
await startMcpServer();
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
if (!TOOL_NAMES.includes(cmd ?? ""))
|
|
52
|
+
usage();
|
|
53
|
+
if (rest.length > 1)
|
|
54
|
+
usage();
|
|
55
|
+
let args = {};
|
|
56
|
+
if (rest.length === 1) {
|
|
57
|
+
try {
|
|
58
|
+
const parsed = JSON.parse(rest[0]);
|
|
59
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
60
|
+
usage();
|
|
61
|
+
}
|
|
62
|
+
args = parsed;
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
console.error("error: argument must be a JSON object string");
|
|
66
|
+
process.exit(2);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
const result = await callTool(cmd, args);
|
|
70
|
+
if (result.isError) {
|
|
71
|
+
console.error(`error: ${firstText(result)}`);
|
|
72
|
+
process.exit(1);
|
|
73
|
+
}
|
|
74
|
+
console.log(typeof result.content === "string"
|
|
75
|
+
? result.content
|
|
76
|
+
: JSON.stringify(result.content, null, 2));
|
|
77
|
+
}
|
|
78
|
+
void main();
|
|
79
|
+
//# sourceMappingURL=cli.js.map
|
package/dist/cli.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* modelrelay core: a forwarding client for the hosted
|
|
3
|
+
* Synap MCP server.
|
|
4
|
+
*
|
|
5
|
+
* No model inference happens on this machine. tools/list is
|
|
6
|
+
* answered from the literal definitions below, which
|
|
7
|
+
* reproduce a live capture of the upstream server word
|
|
8
|
+
* for word (name, title, description and inputSchema
|
|
9
|
+
* exactly as captured; `synap_chat_completion` requires
|
|
10
|
+
* `model` and `messages`, `synap_get_model` requires
|
|
11
|
+
* `model`, `synap_estimate_cost` requires `model` and
|
|
12
|
+
* `prompt_tokens`, and `synap_list_models` declares no
|
|
13
|
+
* `required` field). A tools/call travels upstream over a
|
|
14
|
+
* streamable-HTTP JSON-RPC session: an initialize POST, a
|
|
15
|
+
* notifications/initialized POST, then the tools/call
|
|
16
|
+
* POST, carrying the Mcp-Session-Id response header on
|
|
17
|
+
* later requests when the upstream issues one. The
|
|
18
|
+
* session opens lazily on the first forwarded call and
|
|
19
|
+
* is reused for the client's lifetime.
|
|
20
|
+
*
|
|
21
|
+
* The endpoint is pinned to
|
|
22
|
+
* https://pool.linkrra.com/mcp and may be
|
|
23
|
+
* replaced with the SYNAP_MCP_URL environment
|
|
24
|
+
* variable (that override is a labelled
|
|
25
|
+
* reconstruction). The registry record pins a secret
|
|
26
|
+
* `Authorization` header on its remote and the capture
|
|
27
|
+
* was taken keyless; when SYNAP_API_KEY is set, every
|
|
28
|
+
* upstream request carries an
|
|
29
|
+
* `Authorization: Bearer <key>` header (that env
|
|
30
|
+
* forwarding is a labelled reconstruction). When it is
|
|
31
|
+
* unset, no key header goes out.
|
|
32
|
+
*
|
|
33
|
+
* A successful upstream result is returned exactly as
|
|
34
|
+
* received, isError flag and extra fields included.
|
|
35
|
+
* Failures — an upstream JSON-RPC error, a non-2xx
|
|
36
|
+
* status, a body that cannot be parsed, or a fetch
|
|
37
|
+
* that throws or times out — become an isError result
|
|
38
|
+
* whose text begins with `modelrelay error:` (that
|
|
39
|
+
* rendering is a labelled reconstruction). Replies may
|
|
40
|
+
* be plain JSON or SSE (`data:` lines); the JSON-RPC
|
|
41
|
+
* message whose id matches the request wins.
|
|
42
|
+
*/
|
|
43
|
+
import type { ForwardDeps, ToolArgs, ToolDef, ToolResult } from "./types.js";
|
|
44
|
+
export declare const VERSION = "0.1.0";
|
|
45
|
+
export declare const DEFAULT_ENDPOINT_URL = "https://pool.linkrra.com/mcp";
|
|
46
|
+
export declare const DEFAULT_TIMEOUT_MS = 30000;
|
|
47
|
+
export declare const PROTOCOL_VERSION = "2025-03-26";
|
|
48
|
+
export declare const TOOL_DEFS: ToolDef[];
|
|
49
|
+
export declare const TOOL_NAMES: string[];
|
|
50
|
+
/** Whether `name` belongs to the captured surface. */
|
|
51
|
+
export declare function isKnownTool(name: string): boolean;
|
|
52
|
+
/**
|
|
53
|
+
* Read a JSON-RPC response body, tolerating SSE framing.
|
|
54
|
+
* Returns the message whose id matches `expectedId` when
|
|
55
|
+
* present, otherwise the last parseable message, otherwise
|
|
56
|
+
* null when nothing in the body parses.
|
|
57
|
+
*/
|
|
58
|
+
export declare function parseRpcBody(text: string, expectedId?: number): Record<string, unknown> | null;
|
|
59
|
+
/**
|
|
60
|
+
* A forwarding client holding a single upstream session,
|
|
61
|
+
* opened lazily on first use and reused from then on
|
|
62
|
+
* (for the process lifetime in the MCP server, or for a
|
|
63
|
+
* single invocation in the CLI).
|
|
64
|
+
*/
|
|
65
|
+
export declare class ModelrelayClient {
|
|
66
|
+
private readonly deps;
|
|
67
|
+
private sessionId;
|
|
68
|
+
private ready;
|
|
69
|
+
private sessionPromise;
|
|
70
|
+
private idCounter;
|
|
71
|
+
constructor(deps?: ForwardDeps);
|
|
72
|
+
private fetchFn;
|
|
73
|
+
private endpointUrl;
|
|
74
|
+
private apiKey;
|
|
75
|
+
private timeoutMs;
|
|
76
|
+
private nextId;
|
|
77
|
+
private post;
|
|
78
|
+
private ensureSession;
|
|
79
|
+
/** Forward one tool call; upstream failures come back as isError results. */
|
|
80
|
+
callTool(name: string, args: ToolArgs): Promise<ToolResult>;
|
|
81
|
+
}
|
|
82
|
+
/** Build a forwarding client; tests inject a fake fetch and env through deps. */
|
|
83
|
+
export declare function createClient(deps?: ForwardDeps): ModelrelayClient;
|
|
84
|
+
/** One-shot helper: a fresh client for a single call (the CLI path). */
|
|
85
|
+
export declare function callTool(name: string, args: ToolArgs, deps?: ForwardDeps): Promise<ToolResult>;
|
package/dist/forward.js
ADDED
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
export const VERSION = "0.1.0";
|
|
2
|
+
export const DEFAULT_ENDPOINT_URL = "https://pool.linkrra.com/mcp";
|
|
3
|
+
export const DEFAULT_TIMEOUT_MS = 30000;
|
|
4
|
+
export const PROTOCOL_VERSION = "2025-03-26";
|
|
5
|
+
/* ------------------------------------------------------------------ */
|
|
6
|
+
/* Tool definitions (tools/list literals, verbatim from the live */
|
|
7
|
+
/* capture in harness/fixtures/synap-tools-original.json; */
|
|
8
|
+
/* generated programmatically by parsing and re-serialising the */
|
|
9
|
+
/* fixture JSON, never retyped) */
|
|
10
|
+
/* ------------------------------------------------------------------ */
|
|
11
|
+
export const TOOL_DEFS = [
|
|
12
|
+
{
|
|
13
|
+
"name": "synap_chat_completion",
|
|
14
|
+
"title": "Run a chat completion on Synap",
|
|
15
|
+
"description": "Send a conversation to a model on Synap, Linkrra's OpenAI-compatible inference API, and get the model's reply. Use this when you need text generated, code written or a question answered by one of the open models Synap serves. This is the only tool here that costs money: it bills prompt and completion tokens to the Synap API key supplied as the Authorization: Bearer header on the MCP connection. With no key it returns an error and nothing is spent; a key with no balance returns insufficient_credits. Call synap_estimate_cost first if the price matters. Returns the standard OpenAI chat.completion JSON (choices[0].message.content holds the reply, usage holds token counts). The call is not streamed and keeps no memory between calls — send the full message history each time.",
|
|
16
|
+
"inputSchema": {
|
|
17
|
+
"type": "object",
|
|
18
|
+
"properties": {
|
|
19
|
+
"model": {
|
|
20
|
+
"type": "string",
|
|
21
|
+
"description": "Model id exactly as returned by synap_list_models, e.g. \"qwen3-coder-30b\". Use \"synap-v1\" to let Synap pick the cheapest model that can handle the request."
|
|
22
|
+
},
|
|
23
|
+
"messages": {
|
|
24
|
+
"type": "array",
|
|
25
|
+
"description": "The conversation so far, oldest first. The last message is normally the user turn to answer.",
|
|
26
|
+
"minItems": 1,
|
|
27
|
+
"items": {
|
|
28
|
+
"type": "object",
|
|
29
|
+
"properties": {
|
|
30
|
+
"role": {
|
|
31
|
+
"type": "string",
|
|
32
|
+
"enum": [
|
|
33
|
+
"system",
|
|
34
|
+
"user",
|
|
35
|
+
"assistant"
|
|
36
|
+
],
|
|
37
|
+
"description": "Who wrote the message: system (instructions), user, or assistant (an earlier model reply)."
|
|
38
|
+
},
|
|
39
|
+
"content": {
|
|
40
|
+
"type": "string",
|
|
41
|
+
"description": "The message text."
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
"required": [
|
|
45
|
+
"role",
|
|
46
|
+
"content"
|
|
47
|
+
]
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
"temperature": {
|
|
51
|
+
"type": "number",
|
|
52
|
+
"minimum": 0,
|
|
53
|
+
"maximum": 2,
|
|
54
|
+
"description": "Sampling randomness, 0 to 2. Lower is more deterministic. Omit to use the model default."
|
|
55
|
+
},
|
|
56
|
+
"max_tokens": {
|
|
57
|
+
"type": "integer",
|
|
58
|
+
"minimum": 1,
|
|
59
|
+
"description": "Upper limit on tokens generated in the reply. Omit to use the model default; lower it to cap cost."
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
"required": [
|
|
63
|
+
"model",
|
|
64
|
+
"messages"
|
|
65
|
+
]
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
"name": "synap_list_models",
|
|
70
|
+
"title": "List Synap models and prices",
|
|
71
|
+
"description": "List the models Synap serves right now, each with its price per million tokens, context window and what it is good at. Use this to choose a model id before calling synap_chat_completion, or to compare prices. Free, read-only, no API key needed, and safe to call repeatedly; the list is live, so models can appear or go between calls. Returns {\"object\":\"list\",\"count\":N,\"data\":[{id,name,capability,tier,context_tokens,max_output_tokens,pricing,data_policy}]}; pricing is USD per million prompt and completion tokens and is absent for the auto-routing ids (\"synap-v1\"), which bill at the price of the model they route to. For every field of one model use synap_get_model.",
|
|
72
|
+
"inputSchema": {
|
|
73
|
+
"type": "object",
|
|
74
|
+
"properties": {
|
|
75
|
+
"query": {
|
|
76
|
+
"type": "string",
|
|
77
|
+
"description": "Optional case-insensitive filter matched against model id, name and capability, e.g. \"coder\", \"qwen\" or \"vision\". Omit to list everything."
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
"name": "synap_get_model",
|
|
84
|
+
"title": "Get one Synap model in full",
|
|
85
|
+
"description": "Return the complete catalogue record for one Synap model: pricing, context and output limits, supported parameters, quantisation, data-retention policy and readiness. Use this after synap_list_models when you need details the summary leaves out. Free, read-only and needs no API key. Returns the raw model object, or an error naming the id if Synap does not serve it.",
|
|
86
|
+
"inputSchema": {
|
|
87
|
+
"type": "object",
|
|
88
|
+
"properties": {
|
|
89
|
+
"model": {
|
|
90
|
+
"type": "string",
|
|
91
|
+
"description": "Model id exactly as returned by synap_list_models."
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
"required": [
|
|
95
|
+
"model"
|
|
96
|
+
]
|
|
97
|
+
}
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
"name": "synap_estimate_cost",
|
|
101
|
+
"title": "Estimate the cost of a Synap call",
|
|
102
|
+
"description": "Work out what a chat completion would cost on Synap from token counts, using the live price list. Use this before synap_chat_completion when budget matters, or to compare models for a job. Free, read-only, needs no API key and runs no model. Returns {model, prompt_tokens, completion_tokens, prompt_usd, completion_usd, total_usd, pricing}. It is an estimate: the real charge uses the token counts the model reports afterwards. Auto-routing ids such as \"synap-v1\" have no fixed price and return an error saying so.",
|
|
103
|
+
"inputSchema": {
|
|
104
|
+
"type": "object",
|
|
105
|
+
"properties": {
|
|
106
|
+
"model": {
|
|
107
|
+
"type": "string",
|
|
108
|
+
"description": "Model id exactly as returned by synap_list_models."
|
|
109
|
+
},
|
|
110
|
+
"prompt_tokens": {
|
|
111
|
+
"type": "integer",
|
|
112
|
+
"minimum": 0,
|
|
113
|
+
"description": "Tokens you will send (system + conversation). Roughly 4 characters of English per token."
|
|
114
|
+
},
|
|
115
|
+
"completion_tokens": {
|
|
116
|
+
"type": "integer",
|
|
117
|
+
"minimum": 0,
|
|
118
|
+
"description": "Tokens you expect back. Use your max_tokens for a worst case. Defaults to 0."
|
|
119
|
+
}
|
|
120
|
+
},
|
|
121
|
+
"required": [
|
|
122
|
+
"model",
|
|
123
|
+
"prompt_tokens"
|
|
124
|
+
]
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
];
|
|
128
|
+
export const TOOL_NAMES = TOOL_DEFS.map((t) => t.name);
|
|
129
|
+
/** Whether `name` belongs to the captured surface. */
|
|
130
|
+
export function isKnownTool(name) {
|
|
131
|
+
return TOOL_NAMES.includes(name);
|
|
132
|
+
}
|
|
133
|
+
/* ------------------------------------------------------------------ */
|
|
134
|
+
/* Forwarding client */
|
|
135
|
+
/* ------------------------------------------------------------------ */
|
|
136
|
+
function errorResult(detail) {
|
|
137
|
+
return {
|
|
138
|
+
content: [{ type: "text", text: `modelrelay error: ${detail}` }],
|
|
139
|
+
isError: true,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
function rpcErrorMessage(error) {
|
|
143
|
+
if (error !== null && typeof error === "object") {
|
|
144
|
+
const message = error.message;
|
|
145
|
+
if (typeof message === "string" && message !== "")
|
|
146
|
+
return message;
|
|
147
|
+
return JSON.stringify(error);
|
|
148
|
+
}
|
|
149
|
+
return String(error);
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Read a JSON-RPC response body, tolerating SSE framing.
|
|
153
|
+
* Returns the message whose id matches `expectedId` when
|
|
154
|
+
* present, otherwise the last parseable message, otherwise
|
|
155
|
+
* null when nothing in the body parses.
|
|
156
|
+
*/
|
|
157
|
+
export function parseRpcBody(text, expectedId) {
|
|
158
|
+
const trimmed = text.trim();
|
|
159
|
+
if (trimmed === "")
|
|
160
|
+
return null;
|
|
161
|
+
const candidates = [];
|
|
162
|
+
const tryPush = (payload) => {
|
|
163
|
+
try {
|
|
164
|
+
const parsed = JSON.parse(payload);
|
|
165
|
+
if (parsed !== null && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
166
|
+
candidates.push(parsed);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
catch {
|
|
170
|
+
// Not a JSON payload; skip this candidate.
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
if (trimmed.includes("data:")) {
|
|
174
|
+
for (const line of trimmed.split("\n")) {
|
|
175
|
+
const t = line.trim();
|
|
176
|
+
if (!t.startsWith("data:"))
|
|
177
|
+
continue;
|
|
178
|
+
const payload = t.slice("data:".length).trim();
|
|
179
|
+
if (payload !== "" && payload !== "[DONE]")
|
|
180
|
+
tryPush(payload);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
else {
|
|
184
|
+
tryPush(trimmed);
|
|
185
|
+
}
|
|
186
|
+
if (candidates.length === 0)
|
|
187
|
+
return null;
|
|
188
|
+
if (expectedId !== undefined) {
|
|
189
|
+
const match = candidates.find((c) => c.id === expectedId);
|
|
190
|
+
if (match !== undefined)
|
|
191
|
+
return match;
|
|
192
|
+
}
|
|
193
|
+
return candidates[candidates.length - 1];
|
|
194
|
+
}
|
|
195
|
+
async function defaultFetch(url, init) {
|
|
196
|
+
return fetch(url, init);
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* A forwarding client holding a single upstream session,
|
|
200
|
+
* opened lazily on first use and reused from then on
|
|
201
|
+
* (for the process lifetime in the MCP server, or for a
|
|
202
|
+
* single invocation in the CLI).
|
|
203
|
+
*/
|
|
204
|
+
export class ModelrelayClient {
|
|
205
|
+
deps;
|
|
206
|
+
sessionId = null;
|
|
207
|
+
ready = false;
|
|
208
|
+
sessionPromise = null;
|
|
209
|
+
idCounter = 0;
|
|
210
|
+
constructor(deps = {}) {
|
|
211
|
+
this.deps = deps;
|
|
212
|
+
}
|
|
213
|
+
fetchFn() {
|
|
214
|
+
return this.deps.fetchFn ?? defaultFetch;
|
|
215
|
+
}
|
|
216
|
+
endpointUrl() {
|
|
217
|
+
if (this.deps.url && this.deps.url.trim() !== "")
|
|
218
|
+
return this.deps.url.trim();
|
|
219
|
+
const raw = (this.deps.env ?? process.env).SYNAP_MCP_URL;
|
|
220
|
+
if (raw && raw.trim() !== "")
|
|
221
|
+
return raw.trim();
|
|
222
|
+
return DEFAULT_ENDPOINT_URL;
|
|
223
|
+
}
|
|
224
|
+
apiKey() {
|
|
225
|
+
const raw = (this.deps.env ?? process.env).SYNAP_API_KEY;
|
|
226
|
+
if (raw && raw.trim() !== "")
|
|
227
|
+
return raw.trim();
|
|
228
|
+
return null;
|
|
229
|
+
}
|
|
230
|
+
timeoutMs() {
|
|
231
|
+
if (typeof this.deps.timeoutMs === "number" && this.deps.timeoutMs > 0) {
|
|
232
|
+
return this.deps.timeoutMs;
|
|
233
|
+
}
|
|
234
|
+
return DEFAULT_TIMEOUT_MS;
|
|
235
|
+
}
|
|
236
|
+
nextId() {
|
|
237
|
+
this.idCounter += 1;
|
|
238
|
+
return this.idCounter;
|
|
239
|
+
}
|
|
240
|
+
async post(body, withSession) {
|
|
241
|
+
const headers = {
|
|
242
|
+
"Content-Type": "application/json",
|
|
243
|
+
Accept: "application/json, text/event-stream",
|
|
244
|
+
};
|
|
245
|
+
const key = this.apiKey();
|
|
246
|
+
if (key !== null)
|
|
247
|
+
headers["Authorization"] = `Bearer ${key}`;
|
|
248
|
+
if (withSession && this.sessionId !== null) {
|
|
249
|
+
headers["Mcp-Session-Id"] = this.sessionId;
|
|
250
|
+
}
|
|
251
|
+
const controller = new AbortController();
|
|
252
|
+
const timer = setTimeout(() => controller.abort(), this.timeoutMs());
|
|
253
|
+
try {
|
|
254
|
+
return await this.fetchFn()(this.endpointUrl(), {
|
|
255
|
+
method: "POST",
|
|
256
|
+
headers,
|
|
257
|
+
body: JSON.stringify(body),
|
|
258
|
+
signal: controller.signal,
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
finally {
|
|
262
|
+
clearTimeout(timer);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
async ensureSession() {
|
|
266
|
+
if (this.ready)
|
|
267
|
+
return;
|
|
268
|
+
if (this.sessionPromise === null) {
|
|
269
|
+
this.sessionPromise = (async () => {
|
|
270
|
+
const initId = this.nextId();
|
|
271
|
+
const initRes = await this.post({
|
|
272
|
+
jsonrpc: "2.0",
|
|
273
|
+
id: initId,
|
|
274
|
+
method: "initialize",
|
|
275
|
+
params: {
|
|
276
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
277
|
+
capabilities: {},
|
|
278
|
+
clientInfo: { name: "modelrelay", version: VERSION },
|
|
279
|
+
},
|
|
280
|
+
}, false);
|
|
281
|
+
const sessionId = initRes.headers.get("mcp-session-id");
|
|
282
|
+
if (sessionId)
|
|
283
|
+
this.sessionId = sessionId;
|
|
284
|
+
const initText = await initRes.text();
|
|
285
|
+
const initParsed = parseRpcBody(initText, initId);
|
|
286
|
+
if (initParsed !== null && initParsed.error) {
|
|
287
|
+
throw new Error(rpcErrorMessage(initParsed.error));
|
|
288
|
+
}
|
|
289
|
+
if (initRes.status < 200 || initRes.status >= 300) {
|
|
290
|
+
throw new Error(`HTTP ${initRes.status}`);
|
|
291
|
+
}
|
|
292
|
+
if (initParsed === null && initText.trim() !== "") {
|
|
293
|
+
throw new Error("malformed response");
|
|
294
|
+
}
|
|
295
|
+
const initializedRes = await this.post({ jsonrpc: "2.0", method: "notifications/initialized" }, true);
|
|
296
|
+
try {
|
|
297
|
+
await initializedRes.text();
|
|
298
|
+
}
|
|
299
|
+
catch {
|
|
300
|
+
// Nothing needed from the notification response body.
|
|
301
|
+
}
|
|
302
|
+
this.ready = true;
|
|
303
|
+
})();
|
|
304
|
+
}
|
|
305
|
+
try {
|
|
306
|
+
await this.sessionPromise;
|
|
307
|
+
}
|
|
308
|
+
catch (err) {
|
|
309
|
+
this.sessionPromise = null;
|
|
310
|
+
this.sessionId = null;
|
|
311
|
+
throw err;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
/** Forward one tool call; upstream failures come back as isError results. */
|
|
315
|
+
async callTool(name, args) {
|
|
316
|
+
try {
|
|
317
|
+
await this.ensureSession();
|
|
318
|
+
}
|
|
319
|
+
catch (err) {
|
|
320
|
+
return errorResult(err instanceof Error ? err.message : String(err));
|
|
321
|
+
}
|
|
322
|
+
const callId = this.nextId();
|
|
323
|
+
let callRes;
|
|
324
|
+
try {
|
|
325
|
+
callRes = await this.post({
|
|
326
|
+
jsonrpc: "2.0",
|
|
327
|
+
id: callId,
|
|
328
|
+
method: "tools/call",
|
|
329
|
+
params: { name, arguments: args ?? {} },
|
|
330
|
+
}, true);
|
|
331
|
+
}
|
|
332
|
+
catch (err) {
|
|
333
|
+
return errorResult(`request failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
334
|
+
}
|
|
335
|
+
const parsed = parseRpcBody(await callRes.text(), callId);
|
|
336
|
+
if (parsed !== null && parsed.error) {
|
|
337
|
+
return errorResult(rpcErrorMessage(parsed.error));
|
|
338
|
+
}
|
|
339
|
+
if (callRes.status < 200 || callRes.status >= 300) {
|
|
340
|
+
return errorResult(`HTTP ${callRes.status}`);
|
|
341
|
+
}
|
|
342
|
+
if (parsed === null)
|
|
343
|
+
return errorResult("malformed response");
|
|
344
|
+
const result = parsed.result;
|
|
345
|
+
if (result === undefined || result === null || typeof result !== "object") {
|
|
346
|
+
return errorResult("malformed response");
|
|
347
|
+
}
|
|
348
|
+
// Hand the upstream result back exactly as received.
|
|
349
|
+
return result;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
/** Build a forwarding client; tests inject a fake fetch and env through deps. */
|
|
353
|
+
export function createClient(deps = {}) {
|
|
354
|
+
return new ModelrelayClient(deps);
|
|
355
|
+
}
|
|
356
|
+
/** One-shot helper: a fresh client for a single call (the CLI path). */
|
|
357
|
+
export async function callTool(name, args, deps = {}) {
|
|
358
|
+
return new ModelrelayClient(deps).callTool(name, args);
|
|
359
|
+
}
|
|
360
|
+
//# sourceMappingURL=forward.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"forward.js","sourceRoot":"","sources":["../src/forward.ts"],"names":[],"mappings":"AAmDA,MAAM,CAAC,MAAM,OAAO,GAAG,OAAO,CAAC;AAE/B,MAAM,CAAC,MAAM,oBAAoB,GAAG,8BAA8B,CAAC;AAEnE,MAAM,CAAC,MAAM,kBAAkB,GAAG,KAAK,CAAC;AAExC,MAAM,CAAC,MAAM,gBAAgB,GAAG,YAAY,CAAC;AAE7C,wEAAwE;AACxE,yEAAyE;AACzE,yEAAyE;AACzE,yEAAyE;AACzE,yEAAyE;AACzE,wEAAwE;AAExE,MAAM,CAAC,MAAM,SAAS,GAAc;IAClC;QACE,MAAM,EAAE,uBAAuB;QAC/B,OAAO,EAAE,gCAAgC;QACzC,aAAa,EAAE,6wBAA6wB;QAC5xB,aAAa,EAAE;YACb,MAAM,EAAE,QAAQ;YAChB,YAAY,EAAE;gBACZ,OAAO,EAAE;oBACP,MAAM,EAAE,QAAQ;oBAChB,aAAa,EAAE,iKAAiK;iBACjL;gBACD,UAAU,EAAE;oBACV,MAAM,EAAE,OAAO;oBACf,aAAa,EAAE,8FAA8F;oBAC7G,UAAU,EAAE,CAAC;oBACb,OAAO,EAAE;wBACP,MAAM,EAAE,QAAQ;wBAChB,YAAY,EAAE;4BACZ,MAAM,EAAE;gCACN,MAAM,EAAE,QAAQ;gCAChB,MAAM,EAAE;oCACN,QAAQ;oCACR,MAAM;oCACN,WAAW;iCACZ;gCACD,aAAa,EAAE,4FAA4F;6BAC5G;4BACD,SAAS,EAAE;gCACT,MAAM,EAAE,QAAQ;gCAChB,aAAa,EAAE,mBAAmB;6BACnC;yBACF;wBACD,UAAU,EAAE;4BACV,MAAM;4BACN,SAAS;yBACV;qBACF;iBACF;gBACD,aAAa,EAAE;oBACb,MAAM,EAAE,QAAQ;oBAChB,SAAS,EAAE,CAAC;oBACZ,SAAS,EAAE,CAAC;oBACZ,aAAa,EAAE,0FAA0F;iBAC1G;gBACD,YAAY,EAAE;oBACZ,MAAM,EAAE,SAAS;oBACjB,SAAS,EAAE,CAAC;oBACZ,aAAa,EAAE,oGAAoG;iBACpH;aACF;YACD,UAAU,EAAE;gBACV,OAAO;gBACP,UAAU;aACX;SACF;KACF;IACD;QACE,MAAM,EAAE,mBAAmB;QAC3B,OAAO,EAAE,8BAA8B;QACvC,aAAa,EAAE,yqBAAyqB;QACxrB,aAAa,EAAE;YACb,MAAM,EAAE,QAAQ;YAChB,YAAY,EAAE;gBACZ,OAAO,EAAE;oBACP,MAAM,EAAE,QAAQ;oBAChB,aAAa,EAAE,kJAAkJ;iBAClK;aACF;SACF;KACF;IACD;QACE,MAAM,EAAE,iBAAiB;QACzB,OAAO,EAAE,6BAA6B;QACtC,aAAa,EAAE,iXAAiX;QAChY,aAAa,EAAE;YACb,MAAM,EAAE,QAAQ;YAChB,YAAY,EAAE;gBACZ,OAAO,EAAE;oBACP,MAAM,EAAE,QAAQ;oBAChB,aAAa,EAAE,oDAAoD;iBACpE;aACF;YACD,UAAU,EAAE;gBACV,OAAO;aACR;SACF;KACF;IACD;QACE,MAAM,EAAE,qBAAqB;QAC7B,OAAO,EAAE,mCAAmC;QAC5C,aAAa,EAAE,sgBAAsgB;QACrhB,aAAa,EAAE;YACb,MAAM,EAAE,QAAQ;YAChB,YAAY,EAAE;gBACZ,OAAO,EAAE;oBACP,MAAM,EAAE,QAAQ;oBAChB,aAAa,EAAE,oDAAoD;iBACpE;gBACD,eAAe,EAAE;oBACf,MAAM,EAAE,SAAS;oBACjB,SAAS,EAAE,CAAC;oBACZ,aAAa,EAAE,0FAA0F;iBAC1G;gBACD,mBAAmB,EAAE;oBACnB,MAAM,EAAE,SAAS;oBACjB,SAAS,EAAE,CAAC;oBACZ,aAAa,EAAE,8EAA8E;iBAC9F;aACF;YACD,UAAU,EAAE;gBACV,OAAO;gBACP,eAAe;aAChB;SACF;KACF;CACF,CAAC;AAEF,MAAM,CAAC,MAAM,UAAU,GAAG,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;AAEvD,sDAAsD;AACtD,MAAM,UAAU,WAAW,CAAC,IAAY;IACtC,OAAO,UAAU,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;AACnC,CAAC;AAED,wEAAwE;AACxE,yEAAyE;AACzE,wEAAwE;AAExE,SAAS,WAAW,CAAC,MAAc;IACjC,OAAO;QACL,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,qBAAqB,MAAM,EAAE,EAAE,CAAC;QAChE,OAAO,EAAE,IAAI;KACd,CAAC;AACJ,CAAC;AAED,SAAS,eAAe,CAAC,KAAc;IACrC,IAAI,KAAK,KAAK,IAAI,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QAChD,MAAM,OAAO,GAAI,KAA+B,CAAC,OAAO,CAAC;QACzD,IAAI,OAAO,OAAO,KAAK,QAAQ,IAAI,OAAO,KAAK,EAAE;YAAE,OAAO,OAAO,CAAC;QAClE,OAAO,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC;IAC/B,CAAC;IACD,OAAO,MAAM,CAAC,KAAK,CAAC,CAAC;AACvB,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,YAAY,CAC1B,IAAY,EACZ,UAAmB;IAEnB,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC;IAC5B,IAAI,OAAO,KAAK,EAAE;QAAE,OAAO,IAAI,CAAC;IAChC,MAAM,UAAU,GAA8B,EAAE,CAAC;IACjD,MAAM,OAAO,GAAG,CAAC,OAAe,EAAQ,EAAE;QACxC,IAAI,CAAC;YACH,MAAM,MAAM,GAAY,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;YAC5C,IAAI,MAAM,KAAK,IAAI,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;gBAC5E,UAAU,CAAC,IAAI,CAAC,MAAiC,CAAC,CAAC;YACrD,CAAC;QACH,CAAC;QAAC,MAAM,CAAC;YACP,2CAA2C;QAC7C,CAAC;IACH,CAAC,CAAC;IACF,IAAI,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAC;QAC9B,KAAK,MAAM,IAAI,IAAI,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC;YACvC,MAAM,CAAC,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC;YACtB,IAAI,CAAC,CAAC,CAAC,UAAU,CAAC,OAAO,CAAC;gBAAE,SAAS;YACrC,MAAM,OAAO,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,CAAC;YAC/C,IAAI,OAAO,KAAK,EAAE,IAAI,OAAO,KAAK,QAAQ;gBAAE,OAAO,CAAC,OAAO,CAAC,CAAC;QAC/D,CAAC;IACH,CAAC;SAAM,CAAC;QACN,OAAO,CAAC,OAAO,CAAC,CAAC;IACnB,CAAC;IACD,IAAI,UAAU,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IACzC,IAAI,UAAU,KAAK,SAAS,EAAE,CAAC;QAC7B,MAAM,KAAK,GAAG,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,UAAU,CAAC,CAAC;QAC1D,IAAI,KAAK,KAAK,SAAS;YAAE,OAAO,KAAK,CAAC;IACxC,CAAC;IACD,OAAO,UAAU,CAAC,UAAU,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;AAC3C,CAAC;AAED,KAAK,UAAU,YAAY,CACzB,GAAW,EACX,IAKC;IAED,OAAO,KAAK,CAAC,GAAG,EAAE,IAAI,CAAiC,CAAC;AAC1D,CAAC;AAED;;;;;GAKG;AACH,MAAM,OAAO,gBAAgB;IACV,IAAI,CAAc;IAC3B,SAAS,GAAkB,IAAI,CAAC;IAChC,KAAK,GAAG,KAAK,CAAC;IACd,cAAc,GAAyB,IAAI,CAAC;IAC5C,SAAS,GAAG,CAAC,CAAC;IAEtB,YAAY,OAAoB,EAAE;QAChC,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;IACnB,CAAC;IAEO,OAAO;QACb,OAAO,IAAI,CAAC,IAAI,CAAC,OAAO,IAAI,YAAY,CAAC;IAC3C,CAAC;IAEO,WAAW;QACjB,IAAI,IAAI,CAAC,IAAI,CAAC,GAAG,IAAI,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,KAAK,EAAE;YAAE,OAAO,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC;QAC9E,MAAM,GAAG,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,IAAI,OAAO,CAAC,GAAG,CAAC,CAAC,aAAa,CAAC;QACzD,IAAI,GAAG,IAAI,GAAG,CAAC,IAAI,EAAE,KAAK,EAAE;YAAE,OAAO,GAAG,CAAC,IAAI,EAAE,CAAC;QAChD,OAAO,oBAAoB,CAAC;IAC9B,CAAC;IAEO,MAAM;QACZ,MAAM,GAAG,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,IAAI,OAAO,CAAC,GAAG,CAAC,CAAC,aAAa,CAAC;QACzD,IAAI,GAAG,IAAI,GAAG,CAAC,IAAI,EAAE,KAAK,EAAE;YAAE,OAAO,GAAG,CAAC,IAAI,EAAE,CAAC;QAChD,OAAO,IAAI,CAAC;IACd,CAAC;IAEO,SAAS;QACf,IAAI,OAAO,IAAI,CAAC,IAAI,CAAC,SAAS,KAAK,QAAQ,IAAI,IAAI,CAAC,IAAI,CAAC,SAAS,GAAG,CAAC,EAAE,CAAC;YACvE,OAAO,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC;QAC7B,CAAC;QACD,OAAO,kBAAkB,CAAC;IAC5B,CAAC;IAEO,MAAM;QACZ,IAAI,CAAC,SAAS,IAAI,CAAC,CAAC;QACpB,OAAO,IAAI,CAAC,SAAS,CAAC;IACxB,CAAC;IAEO,KAAK,CAAC,IAAI,CAChB,IAAa,EACb,WAAoB;QAEpB,MAAM,OAAO,GAA2B;YACtC,cAAc,EAAE,kBAAkB;YAClC,MAAM,EAAE,qCAAqC;SAC9C,CAAC;QACF,MAAM,GAAG,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC;QAC1B,IAAI,GAAG,KAAK,IAAI;YAAE,OAAO,CAAC,eAAe,CAAC,GAAG,UAAU,GAAG,EAAE,CAAC;QAC7D,IAAI,WAAW,IAAI,IAAI,CAAC,SAAS,KAAK,IAAI,EAAE,CAAC;YAC3C,OAAO,CAAC,gBAAgB,CAAC,GAAG,IAAI,CAAC,SAAS,CAAC;QAC7C,CAAC;QACD,MAAM,UAAU,GAAG,IAAI,eAAe,EAAE,CAAC;QACzC,MAAM,KAAK,GAAG,UAAU,CAAC,GAAG,EAAE,CAAC,UAAU,CAAC,KAAK,EAAE,EAAE,IAAI,CAAC,SAAS,EAAE,CAAC,CAAC;QACrE,IAAI,CAAC;YACH,OAAO,MAAM,IAAI,CAAC,OAAO,EAAE,CAAC,IAAI,CAAC,WAAW,EAAE,EAAE;gBAC9C,MAAM,EAAE,MAAM;gBACd,OAAO;gBACP,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC;gBAC1B,MAAM,EAAE,UAAU,CAAC,MAAM;aAC1B,CAAC,CAAC;QACL,CAAC;gBAAS,CAAC;YACT,YAAY,CAAC,KAAK,CAAC,CAAC;QACtB,CAAC;IACH,CAAC;IAEO,KAAK,CAAC,aAAa;QACzB,IAAI,IAAI,CAAC,KAAK;YAAE,OAAO;QACvB,IAAI,IAAI,CAAC,cAAc,KAAK,IAAI,EAAE,CAAC;YACjC,IAAI,CAAC,cAAc,GAAG,CAAC,KAAK,IAAI,EAAE;gBAChC,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC;gBAC7B,MAAM,OAAO,GAAG,MAAM,IAAI,CAAC,IAAI,CAC7B;oBACE,OAAO,EAAE,KAAK;oBACd,EAAE,EAAE,MAAM;oBACV,MAAM,EAAE,YAAY;oBACpB,MAAM,EAAE;wBACN,eAAe,EAAE,gBAAgB;wBACjC,YAAY,EAAE,EAAE;wBAChB,UAAU,EAAE,EAAE,IAAI,EAAE,YAAY,EAAE,OAAO,EAAE,OAAO,EAAE;qBACrD;iBACF,EACD,KAAK,CACN,CAAC;gBACF,MAAM,SAAS,GAAG,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,gBAAgB,CAAC,CAAC;gBACxD,IAAI,SAAS;oBAAE,IAAI,CAAC,SAAS,GAAG,SAAS,CAAC;gBAC1C,MAAM,QAAQ,GAAG,MAAM,OAAO,CAAC,IAAI,EAAE,CAAC;gBACtC,MAAM,UAAU,GAAG,YAAY,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;gBAClD,IAAI,UAAU,KAAK,IAAI,IAAI,UAAU,CAAC,KAAK,EAAE,CAAC;oBAC5C,MAAM,IAAI,KAAK,CAAC,eAAe,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC,CAAC;gBACrD,CAAC;gBACD,IAAI,OAAO,CAAC,MAAM,GAAG,GAAG,IAAI,OAAO,CAAC,MAAM,IAAI,GAAG,EAAE,CAAC;oBAClD,MAAM,IAAI,KAAK,CAAC,QAAQ,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;gBAC5C,CAAC;gBACD,IAAI,UAAU,KAAK,IAAI,IAAI,QAAQ,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC;oBAClD,MAAM,IAAI,KAAK,CAAC,oBAAoB,CAAC,CAAC;gBACxC,CAAC;gBACD,MAAM,cAAc,GAAG,MAAM,IAAI,CAAC,IAAI,CACpC,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,2BAA2B,EAAE,EACvD,IAAI,CACL,CAAC;gBACF,IAAI,CAAC;oBACH,MAAM,cAAc,CAAC,IAAI,EAAE,CAAC;gBAC9B,CAAC;gBAAC,MAAM,CAAC;oBACP,sDAAsD;gBACxD,CAAC;gBACD,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC;YACpB,CAAC,CAAC,EAAE,CAAC;QACP,CAAC;QACD,IAAI,CAAC;YACH,MAAM,IAAI,CAAC,cAAc,CAAC;QAC5B,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,IAAI,CAAC,cAAc,GAAG,IAAI,CAAC;YAC3B,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC;YACtB,MAAM,GAAG,CAAC;QACZ,CAAC;IACH,CAAC;IAED,6EAA6E;IAC7E,KAAK,CAAC,QAAQ,CAAC,IAAY,EAAE,IAAc;QACzC,IAAI,CAAC;YACH,MAAM,IAAI,CAAC,aAAa,EAAE,CAAC;QAC7B,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,OAAO,WAAW,CAAC,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC;QACvE,CAAC;QAED,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC;QAC7B,IAAI,OAA0B,CAAC;QAC/B,IAAI,CAAC;YACH,OAAO,GAAG,MAAM,IAAI,CAAC,IAAI,CACvB;gBACE,OAAO,EAAE,KAAK;gBACd,EAAE,EAAE,MAAM;gBACV,MAAM,EAAE,YAAY;gBACpB,MAAM,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,IAAI,EAAE,EAAE;aACxC,EACD,IAAI,CACL,CAAC;QACJ,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,OAAO,WAAW,CAChB,mBAAmB,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CACtE,CAAC;QACJ,CAAC;QAED,MAAM,MAAM,GAAG,YAAY,CAAC,MAAM,OAAO,CAAC,IAAI,EAAE,EAAE,MAAM,CAAC,CAAC;QAC1D,IAAI,MAAM,KAAK,IAAI,IAAI,MAAM,CAAC,KAAK,EAAE,CAAC;YACpC,OAAO,WAAW,CAAC,eAAe,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;QACpD,CAAC;QACD,IAAI,OAAO,CAAC,MAAM,GAAG,GAAG,IAAI,OAAO,CAAC,MAAM,IAAI,GAAG,EAAE,CAAC;YAClD,OAAO,WAAW,CAAC,QAAQ,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;QAC/C,CAAC;QACD,IAAI,MAAM,KAAK,IAAI;YAAE,OAAO,WAAW,CAAC,oBAAoB,CAAC,CAAC;QAC9D,MAAM,MAAM,GAAG,MAAM,CAAC,MAAgC,CAAC;QACvD,IAAI,MAAM,KAAK,SAAS,IAAI,MAAM,KAAK,IAAI,IAAI,OAAO,MAAM,KAAK,QAAQ,EAAE,CAAC;YAC1E,OAAO,WAAW,CAAC,oBAAoB,CAAC,CAAC;QAC3C,CAAC;QACD,qDAAqD;QACrD,OAAO,MAAM,CAAC;IAChB,CAAC;CACF;AAED,iFAAiF;AACjF,MAAM,UAAU,YAAY,CAAC,OAAoB,EAAE;IACjD,OAAO,IAAI,gBAAgB,CAAC,IAAI,CAAC,CAAC;AACpC,CAAC;AAED,wEAAwE;AACxE,MAAM,CAAC,KAAK,UAAU,QAAQ,CAC5B,IAAY,EACZ,IAAc,EACd,OAAoB,EAAE;IAEtB,OAAO,IAAI,gBAAgB,CAAC,IAAI,CAAC,CAAC,QAAQ,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;AACzD,CAAC"}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAC;AAC3B,cAAc,cAAc,CAAC"}
|
package/dist/mcp.d.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* modelrelay MCP server. Tools: the 4 captured
|
|
3
|
+
* Synap tools (see forward.ts).
|
|
4
|
+
*
|
|
5
|
+
* This file uses the low-level Server API rather than
|
|
6
|
+
* McpServer: the high-level helper rebuilds schemas from
|
|
7
|
+
* zod and rewrites what is served, which would break
|
|
8
|
+
* exact parity with the captured tools/list. Here
|
|
9
|
+
* tools/list is answered locally from the core's literal
|
|
10
|
+
* definitions — the verbatim objects from the upstream
|
|
11
|
+
* capture — and every tools/call is forwarded upstream.
|
|
12
|
+
*
|
|
13
|
+
* A forwarded result returns unchanged, upstream isError
|
|
14
|
+
* flag and extra fields included. An upstream failure
|
|
15
|
+
* arrives as an isError result carrying a
|
|
16
|
+
* `modelrelay error:` text, built by the forwarding
|
|
17
|
+
* client. Unknown tool names are rejected with McpError
|
|
18
|
+
* MethodNotFound, and non-object arguments with McpError
|
|
19
|
+
* InvalidParams, before any network hop is made.
|
|
20
|
+
*/
|
|
21
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
22
|
+
import { type ModelrelayClient } from "./forward.js";
|
|
23
|
+
import type { ForwardDeps, ToolResult } from "./types.js";
|
|
24
|
+
/**
|
|
25
|
+
* Validate and forward a single tools/call through
|
|
26
|
+
* `client`. Exported so this layer can be tested directly
|
|
27
|
+
* without a stdio transport.
|
|
28
|
+
*/
|
|
29
|
+
export declare function handleCallTool(client: ModelrelayClient, name: string, args: unknown): Promise<ToolResult>;
|
|
30
|
+
/** Build the stdio MCP server (transport not yet connected). */
|
|
31
|
+
export declare function createServer(deps?: ForwardDeps): Server;
|
|
32
|
+
/** Connect the MCP server over stdio. */
|
|
33
|
+
export declare function startMcpServer(deps?: ForwardDeps): Promise<void>;
|
package/dist/mcp.js
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* modelrelay MCP server. Tools: the 4 captured
|
|
3
|
+
* Synap tools (see forward.ts).
|
|
4
|
+
*
|
|
5
|
+
* This file uses the low-level Server API rather than
|
|
6
|
+
* McpServer: the high-level helper rebuilds schemas from
|
|
7
|
+
* zod and rewrites what is served, which would break
|
|
8
|
+
* exact parity with the captured tools/list. Here
|
|
9
|
+
* tools/list is answered locally from the core's literal
|
|
10
|
+
* definitions — the verbatim objects from the upstream
|
|
11
|
+
* capture — and every tools/call is forwarded upstream.
|
|
12
|
+
*
|
|
13
|
+
* A forwarded result returns unchanged, upstream isError
|
|
14
|
+
* flag and extra fields included. An upstream failure
|
|
15
|
+
* arrives as an isError result carrying a
|
|
16
|
+
* `modelrelay error:` text, built by the forwarding
|
|
17
|
+
* client. Unknown tool names are rejected with McpError
|
|
18
|
+
* MethodNotFound, and non-object arguments with McpError
|
|
19
|
+
* InvalidParams, before any network hop is made.
|
|
20
|
+
*/
|
|
21
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
22
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
23
|
+
import { CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, McpError, } from "@modelcontextprotocol/sdk/types.js";
|
|
24
|
+
import { createClient, isKnownTool, TOOL_DEFS, VERSION, } from "./forward.js";
|
|
25
|
+
/**
|
|
26
|
+
* Validate and forward a single tools/call through
|
|
27
|
+
* `client`. Exported so this layer can be tested directly
|
|
28
|
+
* without a stdio transport.
|
|
29
|
+
*/
|
|
30
|
+
export async function handleCallTool(client, name, args) {
|
|
31
|
+
if (!isKnownTool(name)) {
|
|
32
|
+
throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`);
|
|
33
|
+
}
|
|
34
|
+
if (args !== undefined &&
|
|
35
|
+
(typeof args !== "object" || args === null || Array.isArray(args))) {
|
|
36
|
+
throw new McpError(ErrorCode.InvalidParams, `Arguments for tool ${name} must be a JSON object`);
|
|
37
|
+
}
|
|
38
|
+
return client.callTool(name, (args ?? {}));
|
|
39
|
+
}
|
|
40
|
+
/** Build the stdio MCP server (transport not yet connected). */
|
|
41
|
+
export function createServer(deps = {}) {
|
|
42
|
+
const client = createClient(deps);
|
|
43
|
+
const server = new Server({ name: "modelrelay", version: VERSION }, { capabilities: { tools: {} } });
|
|
44
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
45
|
+
tools: TOOL_DEFS,
|
|
46
|
+
}));
|
|
47
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
48
|
+
const { name, arguments: args } = request.params;
|
|
49
|
+
const result = await handleCallTool(client, name, args);
|
|
50
|
+
return result;
|
|
51
|
+
});
|
|
52
|
+
return server;
|
|
53
|
+
}
|
|
54
|
+
/** Connect the MCP server over stdio. */
|
|
55
|
+
export async function startMcpServer(deps = {}) {
|
|
56
|
+
await createServer(deps).connect(new StdioServerTransport());
|
|
57
|
+
}
|
|
58
|
+
//# sourceMappingURL=mcp.js.map
|
package/dist/mcp.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mcp.js","sourceRoot":"","sources":["../src/mcp.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AACH,OAAO,EAAE,MAAM,EAAE,MAAM,2CAA2C,CAAC;AACnE,OAAO,EAAE,oBAAoB,EAAE,MAAM,2CAA2C,CAAC;AACjF,OAAO,EACL,qBAAqB,EACrB,SAAS,EACT,sBAAsB,EACtB,QAAQ,GACT,MAAM,oCAAoC,CAAC;AAC5C,OAAO,EACL,YAAY,EACZ,WAAW,EACX,SAAS,EACT,OAAO,GAER,MAAM,cAAc,CAAC;AAGtB;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,cAAc,CAClC,MAAwB,EACxB,IAAY,EACZ,IAAa;IAEb,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,EAAE,CAAC;QACvB,MAAM,IAAI,QAAQ,CAAC,SAAS,CAAC,cAAc,EAAE,iBAAiB,IAAI,EAAE,CAAC,CAAC;IACxE,CAAC;IACD,IACE,IAAI,KAAK,SAAS;QAClB,CAAC,OAAO,IAAI,KAAK,QAAQ,IAAI,IAAI,KAAK,IAAI,IAAI,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,EAClE,CAAC;QACD,MAAM,IAAI,QAAQ,CAChB,SAAS,CAAC,aAAa,EACvB,sBAAsB,IAAI,wBAAwB,CACnD,CAAC;IACJ,CAAC;IACD,OAAO,MAAM,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAC,IAAI,IAAI,EAAE,CAA4B,CAAC,CAAC;AACxE,CAAC;AAED,gEAAgE;AAChE,MAAM,UAAU,YAAY,CAAC,OAAoB,EAAE;IACjD,MAAM,MAAM,GAAG,YAAY,CAAC,IAAI,CAAC,CAAC;IAClC,MAAM,MAAM,GAAG,IAAI,MAAM,CACvB,EAAE,IAAI,EAAE,YAAY,EAAE,OAAO,EAAE,OAAO,EAAE,EACxC,EAAE,YAAY,EAAE,EAAE,KAAK,EAAE,EAAE,EAAE,EAAE,CAChC,CAAC;IAEF,MAAM,CAAC,iBAAiB,CAAC,sBAAsB,EAAE,KAAK,IAAI,EAAE,CAAC,CAAC;QAC5D,KAAK,EAAE,SAAS;KACjB,CAAC,CAAC,CAAC;IAEJ,MAAM,CAAC,iBAAiB,CAAC,qBAAqB,EAAE,KAAK,EAAE,OAAO,EAAE,EAAE;QAChE,MAAM,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC;QACjD,MAAM,MAAM,GAAG,MAAM,cAAc,CAAC,MAAM,EAAE,IAAI,EAAE,IAAI,CAAC,CAAC;QACxD,OAAO,MAAe,CAAC;IACzB,CAAC,CAAC,CAAC;IAEH,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,yCAAyC;AACzC,MAAM,CAAC,KAAK,UAAU,cAAc,CAAC,OAAoB,EAAE;IACzD,MAAM,YAAY,CAAC,IAAI,CAAC,CAAC,OAAO,CAAC,IAAI,oBAAoB,EAAE,CAAC,CAAC;AAC/D,CAAC"}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* modelrelay types.
|
|
3
|
+
*
|
|
4
|
+
* No model inference happens on this machine. The
|
|
5
|
+
* package is a forwarding client: the captured tool
|
|
6
|
+
* surface is kept as data (ToolDef entries served
|
|
7
|
+
* verbatim for tools/list) and each tools/call is sent
|
|
8
|
+
* to the hosted Synap MCP endpoint through an
|
|
9
|
+
* injectable fetch, so the whole test suite can run
|
|
10
|
+
* against a stubbed upstream with no live network.
|
|
11
|
+
*/
|
|
12
|
+
/** One captured tool definition, served exactly as captured for tools/list. */
|
|
13
|
+
export interface ToolDef {
|
|
14
|
+
name: string;
|
|
15
|
+
title?: string;
|
|
16
|
+
description: string;
|
|
17
|
+
inputSchema: Record<string, unknown>;
|
|
18
|
+
[key: string]: unknown;
|
|
19
|
+
}
|
|
20
|
+
/** Arguments for a tool call, as decoded JSON. */
|
|
21
|
+
export type ToolArgs = Record<string, unknown>;
|
|
22
|
+
/** A tools/call result: upstream payloads pass through verbatim; local failures use this shape too. */
|
|
23
|
+
export interface ToolResult {
|
|
24
|
+
content?: unknown;
|
|
25
|
+
isError?: boolean;
|
|
26
|
+
[key: string]: unknown;
|
|
27
|
+
}
|
|
28
|
+
/** The narrow slice of a fetch Response the forwarding client relies on. */
|
|
29
|
+
export interface FetchResponseLike {
|
|
30
|
+
ok: boolean;
|
|
31
|
+
status: number;
|
|
32
|
+
headers: {
|
|
33
|
+
get(name: string): string | null;
|
|
34
|
+
};
|
|
35
|
+
text(): Promise<string>;
|
|
36
|
+
}
|
|
37
|
+
/** Injectable fetch function, so tests never reach the live network. */
|
|
38
|
+
export type FetchFn = (url: string, init?: {
|
|
39
|
+
method?: string;
|
|
40
|
+
headers?: Record<string, string>;
|
|
41
|
+
body?: string;
|
|
42
|
+
signal?: AbortSignal;
|
|
43
|
+
}) => Promise<FetchResponseLike>;
|
|
44
|
+
/** Injectable runtime dependencies for the forwarding client. */
|
|
45
|
+
export interface ForwardDeps {
|
|
46
|
+
/** Fetch implementation. Defaults to the global fetch. */
|
|
47
|
+
fetchFn?: FetchFn;
|
|
48
|
+
/** Environment (SYNAP_MCP_URL, SYNAP_API_KEY). Defaults to process.env. */
|
|
49
|
+
env?: Record<string, string | undefined>;
|
|
50
|
+
/** Endpoint URL override. Defaults to env SYNAP_MCP_URL or the pinned default. */
|
|
51
|
+
url?: string;
|
|
52
|
+
/** Per-request timeout in milliseconds. Defaults to 30000. */
|
|
53
|
+
timeoutMs?: number;
|
|
54
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* modelrelay types.
|
|
3
|
+
*
|
|
4
|
+
* No model inference happens on this machine. The
|
|
5
|
+
* package is a forwarding client: the captured tool
|
|
6
|
+
* surface is kept as data (ToolDef entries served
|
|
7
|
+
* verbatim for tools/list) and each tools/call is sent
|
|
8
|
+
* to the hosted Synap MCP endpoint through an
|
|
9
|
+
* injectable fetch, so the whole test suite can run
|
|
10
|
+
* against a stubbed upstream with no live network.
|
|
11
|
+
*/
|
|
12
|
+
export {};
|
|
13
|
+
//# sourceMappingURL=types.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,41 @@
|
|
|
1
1
|
{
|
|
2
|
+
"bin": {
|
|
3
|
+
"modelrelay": "dist/cli.js"
|
|
4
|
+
},
|
|
5
|
+
"dependencies": {
|
|
6
|
+
"@modelcontextprotocol/sdk": "^1",
|
|
7
|
+
"zod": "^4"
|
|
8
|
+
},
|
|
9
|
+
"description": "A local stdio bridge to the hosted Synap MCP service: chat completions on open AI models, a live model catalogue with prices, full detail for one model and cost estimates. One bin serves a small CLI and the MCP server.",
|
|
10
|
+
"devDependencies": {
|
|
11
|
+
"@types/node": "^20",
|
|
12
|
+
"tsx": "^4",
|
|
13
|
+
"typescript": "^5"
|
|
14
|
+
},
|
|
15
|
+
"engines": {
|
|
16
|
+
"node": ">=20"
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"dist",
|
|
20
|
+
"server.json"
|
|
21
|
+
],
|
|
22
|
+
"homepage": "https://github.com/CloudCorpRecords/modelrelay#readme",
|
|
23
|
+
"license": "MIT",
|
|
24
|
+
"mcpName": "io.github.CloudCorpRecords/modelrelay",
|
|
2
25
|
"name": "@reneturcios/modelrelay",
|
|
3
|
-
"
|
|
4
|
-
"
|
|
5
|
-
|
|
6
|
-
}
|
|
26
|
+
"private": false,
|
|
27
|
+
"publishConfig": {
|
|
28
|
+
"access": "public"
|
|
29
|
+
},
|
|
30
|
+
"repository": {
|
|
31
|
+
"type": "git",
|
|
32
|
+
"url": "git+https://github.com/CloudCorpRecords/modelrelay.git"
|
|
33
|
+
},
|
|
34
|
+
"scripts": {
|
|
35
|
+
"build": "tsc",
|
|
36
|
+
"dev": "tsx src/cli.ts",
|
|
37
|
+
"test": "tsx --test test/*.test.ts"
|
|
38
|
+
},
|
|
39
|
+
"type": "module",
|
|
40
|
+
"version": "0.1.0"
|
|
41
|
+
}
|
package/server.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
|
|
3
|
+
"description": "Open AI models through one API: chat completions, live model catalogue, prices, cost estimates.",
|
|
4
|
+
"name": "io.github.CloudCorpRecords/modelrelay",
|
|
5
|
+
"packages": [
|
|
6
|
+
{
|
|
7
|
+
"identifier": "@reneturcios/modelrelay",
|
|
8
|
+
"packageArguments": [
|
|
9
|
+
{
|
|
10
|
+
"type": "positional",
|
|
11
|
+
"value": "mcp"
|
|
12
|
+
}
|
|
13
|
+
],
|
|
14
|
+
"registryBaseUrl": "https://registry.npmjs.org",
|
|
15
|
+
"registryType": "npm",
|
|
16
|
+
"runtimeHint": "npx",
|
|
17
|
+
"transport": {
|
|
18
|
+
"type": "stdio"
|
|
19
|
+
},
|
|
20
|
+
"version": "0.1.0"
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
"identifier": "ghcr.io/cloudcorprecords/modelrelay:0.1.0",
|
|
24
|
+
"packageArguments": [
|
|
25
|
+
{
|
|
26
|
+
"type": "positional",
|
|
27
|
+
"value": "mcp"
|
|
28
|
+
}
|
|
29
|
+
],
|
|
30
|
+
"registryType": "oci",
|
|
31
|
+
"runtimeArguments": [
|
|
32
|
+
{
|
|
33
|
+
"name": "--rm",
|
|
34
|
+
"type": "named"
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
"name": "-i",
|
|
38
|
+
"type": "named"
|
|
39
|
+
}
|
|
40
|
+
],
|
|
41
|
+
"runtimeHint": "docker",
|
|
42
|
+
"transport": {
|
|
43
|
+
"type": "stdio"
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
],
|
|
47
|
+
"repository": {
|
|
48
|
+
"source": "github",
|
|
49
|
+
"url": "https://github.com/CloudCorpRecords/modelrelay"
|
|
50
|
+
},
|
|
51
|
+
"version": "0.1.0"
|
|
52
|
+
}
|