@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 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
- # Temporary Holding Version
1
+ # modelrelay
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
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
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
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
@@ -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>;
@@ -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"}
@@ -0,0 +1,2 @@
1
+ export * from "./types.js";
2
+ export * from "./forward.js";
package/dist/index.js ADDED
@@ -0,0 +1,3 @@
1
+ export * from "./types.js";
2
+ export * from "./forward.js";
3
+ //# sourceMappingURL=index.js.map
@@ -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
@@ -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"}
@@ -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
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
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
+ }