growth(day-20): @byok-relay/mcp — MCP server for Claude Desktop + Claude Code - #58
growth(day-20): @byok-relay/mcp — MCP server for Claude Desktop + Claude Code#58alokit-bot wants to merge 3 commits into
Conversation
Add .devcontainer/devcontainer.json: - Uses mcr.microsoft.com/devcontainers/javascript-node:1-20-bookworm - postCreateCommand runs on-create.sh (npm install + auto-generates .env) - Forwards port 3000 with label 'byok-relay API' - VS Code extensions: eslint, prettier, yaml, json - Opens README.md on Codespace start Add .devcontainer/on-create.sh: - npm install - Creates .env with safe dev-only defaults if not present - Prints clear 'what next' instructions on completion Update README.md: - Add 'Open in GitHub Codespaces' badge in header (next to skills.sh) - Add 'Try it instantly' section before Quickstart with Codespaces CTA and one-sentence explain (deps installed, dev .env pre-configured) Enables: zero-friction 'try it' path for developers evaluating the project. GitHub shows Codespaces tab in the Code dropdown once devcontainer.json exists. Metrics 2026-06-27: stars=51 forks=0 watchers=1 clones=258 views=38
…endpoints - openapi.json: full OpenAPI 3.0 spec covering all 7 endpoints (health, users, keys CRUD, relay, openapi.json, openapi.yaml) with request/response schemas, security schemes (RelayToken + AppSecret), provider enum, and worked examples - GET /openapi.json: serves spec as JSON (require-cached, hot-reload safe) - GET /openapi.yaml: serves spec as YAML (js-yaml dump from JSON spec) - llms.txt: API Reference section with managed relay spec URLs - README.md: OpenAPI 3.0 badge in header + spec link at top of API section - deps: js-yaml added for YAML serialisation endpoint AI coding agents can now fetch /openapi.json to discover the full API surface programmatically. Postman/Insomnia/RapidAPI users can import the spec directly. Serves spec at /openapi.json and /openapi.yaml at runtime on both managed relay and self-hosted instances. Metrics: stars=51 forks=0 watchers=1 clones(14d)=259 views(14d)=40
…ude Code Adds packages/mcp/ — a full MCP server that exposes byok-relay as 6 tools for Claude Desktop, Claude Code, Cursor, Windsurf and any MCP client. Tools: byok_relay_health — check relay server health (liveness + readiness) byok_relay_register — register and receive a relay token byok_relay_store_key — store a provider API key (encrypted at rest) byok_relay_request — forward any provider API request through the relay byok_relay_chat — chat completions with unified model routing byok_relay_stats — usage statistics per user / app_id Install via Claude Desktop claude_desktop_config.json: npx -y @byok-relay/mcp (no install step, zero-friction) Also: - llms.txt: MCP Server section + @byok-relay/mcp npm link - README.md: MCP badge in header + 'Use from Claude Desktop' section - package.json: workspaces includes packages/* metrics/history.csv: 2026-06-29 stars=51 forks=0 views=17 clones=116
📝 WalkthroughWalkthroughAdds an OpenAPI 3.0 spec file ( ChangesOpenAPI Spec and Server Routes
MCP Server Package
Codespaces Devcontainer
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 11
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In @.devcontainer/on-create.sh:
- Around line 12-18: The .env bootstrap in on-create.sh is writing a committed
default for ENCRYPTION_SECRET, which should instead be unique per Codespace.
Update the setup flow to generate a fresh random value at creation time rather
than hardcoding the secret, and keep the existing .env generation behavior
otherwise. Use the on-create.sh script as the place to change this, and ensure
the value remains compatible with the encryption logic used by src/index.js and
src/db.js.
In `@llms.txt`:
- Line 108: The `llms.txt` entry currently points to a local relative path that
won’t resolve outside a checkout. Update the `See ... for full setup guide.`
reference to use an absolute README URL instead, and make sure the link still
points to the `packages/mcp/README.md` documentation target.
In `@openapi.json`:
- Around line 308-312: The binary request schema is describing providers that
are not actually allowed by the published contract. Update the
`application/octet-stream` description in the OpenAPI spec so it matches the
supported `provider` enum and does not mention ElevenLabs or Deepgram unless
those providers are added to the enum; keep the wording aligned with the request
body definition around the `provider` field and the binary payload schema.
- Around line 7-10: The license metadata in the OpenAPI spec is inconsistent
with the package manifest, so update the license object in openapi.json to match
the package.json declaration. Locate the existing license block under the
top-level OpenAPI metadata and change the advertised license name (and URL if
needed) so both sources present the same SPDX license for the artifact.
- Around line 230-246: The relay path contract is modeled as a single OpenAPI
path parameter, but the documented examples rely on multi-segment values like
v1/chat/completions. Update the /relay/{provider}/{path} operation in
openapi.json to either redesign the route shape or explicitly document that
{path} must be percent-encoded and make all examples and descriptions
consistent; use the relayRequest operation and path parameter definition as the
main places to fix.
- Around line 96-97: The API description for the registration endpoint currently
tells integrators to store the relay token in localStorage, which is unsafe
guidance for a bearer-equivalent credential. Update the affected OpenAPI
descriptions to use storage-agnostic wording or recommend a safer client-managed
pattern instead, and make sure the same change is applied to every duplicated
registration description referenced by the reviewer, including the one in the
registration operation docs and any other matching entries.
In `@packages/mcp/README.md`:
- Around line 96-98: The fenced code block in README.md is missing a language
hint, which triggers markdownlint MD040. Update the markdown fence around the
“Ask Claude: ...” example to use a text language identifier so the block is
explicitly labeled; this applies to the README example snippet only.
In `@packages/mcp/src/index.js`:
- Around line 124-146: The byok_relay_request schema in inputSchema is missing
support for the passthrough headers required by documented relay calls, so add a
headers field and wire it through the relay request handling. Also make body
optional for non-POST methods in the schema and validation logic, since GET
requests should not require it. Update the byok_relay_request path in
packages/mcp/src/index.js so Anthropic and openai-compatible flows can express
the documented relay calls using the existing provider, path, and method fields
plus the new headers support.
- Around line 33-45: Update the registration flow in relayFetch so the POST
/users call can use APP_SECRET instead of always sending RELAY_TOKEN. Add a way
for byok_relay_register (and any helper it uses) to pass the operator credential
explicitly, and have relayFetch choose the correct Authorization header based on
that call site while leaving normal relay requests on RELAY_TOKEN. Keep the
change localized around relayFetch and byok_relay_register so the registration
path can succeed on APP_SECRET-gated relays.
- Around line 39-51: The relayFetch helper currently waits on fetch with no
timeout, so slow or half-open relay responses can hang requests indefinitely.
Update relayFetch to enforce a finite timeout around the fetch call, using an
AbortController or equivalent timeout mechanism, and make sure the timeout is
applied to the fetch invocation while preserving the existing headers, response
parsing, and returned { status, ok, body } shape.
In `@src/index.js`:
- Around line 141-142: Update the OpenAPI load handlers in the relevant route
logic so the `catch` blocks no longer always return 404 for `OpenAPI spec not
found`; use the existing loader/response path to distinguish missing-file errors
from parse or serialization failures. In the handler(s) around the OpenAPI spec
lookup, return 404 only when the failure is specifically “file not found,” and
return a 500 with an appropriate error response for all other exceptions. Apply
the same fix to both affected catch sites so the behavior is consistent.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 4d58645c-d220-48e3-b57d-caa327b3a9da
⛔ Files ignored due to path filters (3)
metrics/history.csvis excluded by!**/*.csvpackage-lock.jsonis excluded by!**/package-lock.jsonpackages/mcp/package-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (10)
.devcontainer/devcontainer.json.devcontainer/on-create.shREADME.mdllms.txtopenapi.jsonpackage.jsonpackages/mcp/README.mdpackages/mcp/package.jsonpackages/mcp/src/index.jssrc/index.js
| cat > .env <<'EOF' | ||
| # ── Dev-only defaults — NOT for production ────────────────────────────── | ||
| # Generate real secrets with: openssl rand -hex 32 | ||
|
|
||
| # Required: 32+ char secret used to AES-256-GCM encrypt stored API keys | ||
| ENCRYPTION_SECRET=dev-only-change-this-before-any-real-use-32chars | ||
|
|
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
Generate ENCRYPTION_SECRET per Codespace, not from a committed default.
src/index.js:12 and src/db.js:118-126 make this value the root secret for encrypting stored provider keys. Writing the same known secret into every generated .env means anyone with a copy of the encrypted data can decrypt it with the published default. Generate a random secret during setup instead.
Suggested fix
if [ ! -f .env ]; then
echo "🔧 Creating .env with dev-only defaults..."
- cat > .env <<'EOF'
+ ENCRYPTION_SECRET="$(node -e "console.log(require('crypto').randomBytes(32).toString('hex'))")"
+ cat > .env <<EOF
# ── Dev-only defaults — NOT for production ──────────────────────────────
# Generate real secrets with: openssl rand -hex 32
# Required: 32+ char secret used to AES-256-GCM encrypt stored API keys
-ENCRYPTION_SECRET=dev-only-change-this-before-any-real-use-32chars
+ENCRYPTION_SECRET=${ENCRYPTION_SECRET}
# Optional: if set, POST /users requires Authorization: Bearer <APP_SECRET>
# APP_SECRET=📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| cat > .env <<'EOF' | |
| # ── Dev-only defaults — NOT for production ────────────────────────────── | |
| # Generate real secrets with: openssl rand -hex 32 | |
| # Required: 32+ char secret used to AES-256-GCM encrypt stored API keys | |
| ENCRYPTION_SECRET=dev-only-change-this-before-any-real-use-32chars | |
| if [ ! -f .env ]; then | |
| echo "🔧 Creating .env with dev-only defaults..." | |
| ENCRYPTION_SECRET="$(node -e "console.log(require('crypto').randomBytes(32).toString('hex'))")" | |
| cat > .env <<EOF | |
| # ── Dev-only defaults — NOT for production ────────────────────────────── | |
| # Generate real secrets with: openssl rand -hex 32 | |
| # Required: 32+ char secret used to AES-256-GCM encrypt stored API keys | |
| ENCRYPTION_SECRET=${ENCRYPTION_SECRET} | |
| # Optional: if set, POST /users requires Authorization: Bearer <APP_SECRET> | |
| # APP_SECRET= | |
| EOF | |
| fi |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In @.devcontainer/on-create.sh around lines 12 - 18, The .env bootstrap in
on-create.sh is writing a committed default for ENCRYPTION_SECRET, which should
instead be unique per Codespace. Update the setup flow to generate a fresh
random value at creation time rather than hardcoding the secret, and keep the
existing .env generation behavior otherwise. Use the on-create.sh script as the
place to change this, and ensure the value remains compatible with the
encryption logic used by src/index.js and src/db.js.
|
|
||
| Available MCP tools: `byok_relay_health`, `byok_relay_register`, `byok_relay_store_key`, `byok_relay_request`, `byok_relay_chat`, `byok_relay_stats`. | ||
|
|
||
| See `packages/mcp/README.md` for full setup guide. |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Use an absolute MCP README link here.
llms.txt is consumed outside a local checkout, so packages/mcp/README.md is not a resolvable path for most agents. Point this to the GitHub or npm README instead.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@llms.txt` at line 108, The `llms.txt` entry currently points to a local
relative path that won’t resolve outside a checkout. Update the `See ... for
full setup guide.` reference to use an absolute README URL instead, and make
sure the link still points to the `packages/mcp/README.md` documentation target.
| "license": { | ||
| "name": "MIT", | ||
| "url": "https://github.com/avikalpg/byok-relay/blob/main/LICENSE" | ||
| }, |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win
Align the published license metadata with the package manifest.
This spec advertises MIT, but package.json Line 23 now says Apache-2.0. Shipping conflicting license metadata for the same artifact is a contract/compliance problem for downstream consumers and generated docs.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@openapi.json` around lines 7 - 10, The license metadata in the OpenAPI spec
is inconsistent with the package manifest, so update the license object in
openapi.json to match the package.json declaration. Locate the existing license
block under the top-level OpenAPI metadata and change the advertised license
name (and URL if needed) so both sources present the same SPDX license for the
artifact.
| "description": "Creates a user record for an app_id and returns a relay token. Store the token in localStorage — it is the credential for all key and relay operations.\n\nIf the operator has set APP_SECRET, supply `Authorization: Bearer <APP_SECRET>`. Otherwise registration is open (dev mode).\n\nRate limit: 10 registrations/hour per IP.", | ||
| "security": [{ "AppSecret": [] }, {}], |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
Don’t tell integrators to store the relay token in localStorage.
This token is the caller’s bearer-equivalent credential, so recommending localStorage makes it trivially recoverable by any XSS in the consuming app. Please switch this to storage-agnostic wording or recommend a safer pattern.
Also applies to: 373-380
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@openapi.json` around lines 96 - 97, The API description for the registration
endpoint currently tells integrators to store the relay token in localStorage,
which is unsafe guidance for a bearer-equivalent credential. Update the affected
OpenAPI descriptions to use storage-agnostic wording or recommend a safer
client-managed pattern instead, and make sure the same change is applied to
every duplicated registration description referenced by the reviewer, including
the one in the registration operation docs and any other matching entries.
| "/relay/{provider}/{path}": { | ||
| "post": { | ||
| "tags": ["Relay"], | ||
| "operationId": "relayRequest", | ||
| "summary": "Forward a request to an AI provider", | ||
| "description": "Forwards the request body to the AI provider using the user's stored API key. The relay injects the key — your frontend never sees it.\n\n**Path routing:** `{path}` is forwarded verbatim to the provider base URL.\n- Anthropic: `POST /relay/anthropic/v1/messages`\n- OpenAI: `POST /relay/openai/v1/chat/completions`\n- Google Gemini: `POST /relay/google/v1beta/models/gemini-2.0-flash:generateContent`\n\n**Streaming:** set `stream: true` in the request body to receive a Server-Sent Events (SSE) stream piped directly from the provider.\n\n**openai-compatible:** pass `x-relay-base-url` header with the target base URL (SSRF-validated, HTTPS only).\n\nRate limit: 20 AI requests/min per relay token.", | ||
| "security": [{ "RelayToken": [] }], | ||
| "parameters": [ | ||
| { "$ref": "#/components/parameters/provider" }, | ||
| { | ||
| "name": "path", | ||
| "in": "path", | ||
| "required": true, | ||
| "schema": { "type": "string" }, | ||
| "description": "Provider-specific path (e.g. v1/messages, v1/chat/completions)", | ||
| "example": "v1/messages" | ||
| }, |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
{path} cannot represent the documented relay subpaths.
The examples here use raw multi-segment values like v1/chat/completions, but /relay/{provider}/{path} only models a single path segment in OpenAPI. Generated clients will not infer the required slash-encoding, so this contract is inaccurate as written. Please either redesign this part of the API shape or explicitly require percent-encoded slashes and update every example accordingly.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@openapi.json` around lines 230 - 246, The relay path contract is modeled as a
single OpenAPI path parameter, but the documented examples rely on multi-segment
values like v1/chat/completions. Update the /relay/{provider}/{path} operation
in openapi.json to either redesign the route shape or explicitly document that
{path} must be percent-encoded and make all examples and descriptions
consistent; use the relayRequest operation and path parameter definition as the
main places to fix.
| ``` | ||
| Ask Claude: "Use byok_relay_store_key to store my OpenAI key sk-... for provider openai" | ||
| ``` |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Add a language to this fenced block.
This currently trips markdownlint (MD040).
Suggested fix
-```
+```text
Ask Claude: "Use byok_relay_store_key to store my OpenAI key sk-... for provider openai"</details>
<!-- suggestion_start -->
<details>
<summary>📝 Committable suggestion</summary>
> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
```suggestion
🧰 Tools
🪛 markdownlint-cli2 (0.22.1)
[warning] 96-96: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/mcp/README.md` around lines 96 - 98, The fenced code block in
README.md is missing a language hint, which triggers markdownlint MD040. Update
the markdown fence around the “Ask Claude: ...” example to use a text language
identifier so the block is explicitly labeled; this applies to the README
example snippet only.
Source: Linters/SAST tools
| const RELAY_URL = (process.env.RELAY_URL || 'https://relay.byokrelay.com').replace(/\/$/, ''); | ||
| const RELAY_TOKEN = process.env.RELAY_TOKEN || ''; | ||
| const APP_ID = process.env.APP_ID || 'mcp-client'; | ||
|
|
||
| // ─── helpers ───────────────────────────────────────────────────────────────── | ||
|
|
||
| async function relayFetch(path, options = {}) { | ||
| const fetch = globalThis.fetch || (await import('node-fetch')).default; | ||
| const url = `${RELAY_URL}${path}`; | ||
| const headers = { | ||
| 'Content-Type': 'application/json', | ||
| ...(RELAY_TOKEN ? { Authorization: `Bearer ${RELAY_TOKEN}` } : {}), | ||
| ...(options.headers || {}), |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Let registration use APP_SECRET instead of the user relay token.
relayFetch() injects RELAY_TOKEN into every request, but POST /users is the one endpoint that may require the operator's APP_SECRET instead. On an APP_SECRET-gated relay, byok_relay_register cannot succeed because there is no way to send the correct credential.
Suggested fix
const RELAY_URL = (process.env.RELAY_URL || 'https://relay.byokrelay.com').replace(/\/$/, '');
const RELAY_TOKEN = process.env.RELAY_TOKEN || '';
+const APP_SECRET = process.env.APP_SECRET || '';
const APP_ID = process.env.APP_ID || 'mcp-client';
@@
const appId = args.app_id || APP_ID;
const { status, body } = await relayFetch('/users', {
+ headers: APP_SECRET ? { Authorization: `Bearer ${APP_SECRET}` } : {},
method: 'POST',
body: JSON.stringify({ app_id: appId }),
});Also applies to: 217-221
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/mcp/src/index.js` around lines 33 - 45, Update the registration flow
in relayFetch so the POST /users call can use APP_SECRET instead of always
sending RELAY_TOKEN. Add a way for byok_relay_register (and any helper it uses)
to pass the operator credential explicitly, and have relayFetch choose the
correct Authorization header based on that call site while leaving normal relay
requests on RELAY_TOKEN. Keep the change localized around relayFetch and
byok_relay_register so the registration path can succeed on APP_SECRET-gated
relays.
| async function relayFetch(path, options = {}) { | ||
| const fetch = globalThis.fetch || (await import('node-fetch')).default; | ||
| const url = `${RELAY_URL}${path}`; | ||
| const headers = { | ||
| 'Content-Type': 'application/json', | ||
| ...(RELAY_TOKEN ? { Authorization: `Bearer ${RELAY_TOKEN}` } : {}), | ||
| ...(options.headers || {}), | ||
| }; | ||
| const res = await fetch(url, { ...options, headers }); | ||
| const text = await res.text(); | ||
| let body; | ||
| try { body = JSON.parse(text); } catch { body = text; } | ||
| return { status: res.status, ok: res.ok, body }; |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
Add a timeout to relayFetch.
Every tool call blocks on this helper, and fetch has no default timeout. A slow or half-open relay/SSE response will leave the MCP request hanging indefinitely.
Suggested fix
async function relayFetch(path, options = {}) {
const fetch = globalThis.fetch || (await import('node-fetch')).default;
const url = `${RELAY_URL}${path}`;
const headers = {
'Content-Type': 'application/json',
...(RELAY_TOKEN ? { Authorization: `Bearer ${RELAY_TOKEN}` } : {}),
...(options.headers || {}),
};
- const res = await fetch(url, { ...options, headers });
- const text = await res.text();
+ const controller = new AbortController();
+ const timeout = setTimeout(() => controller.abort(), 30000);
+ let res;
+ let text;
+ try {
+ res = await fetch(url, { ...options, headers, signal: controller.signal });
+ text = await res.text();
+ } finally {
+ clearTimeout(timeout);
+ }
let body;
try { body = JSON.parse(text); } catch { body = text; }
return { status: res.status, ok: res.ok, body };
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| async function relayFetch(path, options = {}) { | |
| const fetch = globalThis.fetch || (await import('node-fetch')).default; | |
| const url = `${RELAY_URL}${path}`; | |
| const headers = { | |
| 'Content-Type': 'application/json', | |
| ...(RELAY_TOKEN ? { Authorization: `Bearer ${RELAY_TOKEN}` } : {}), | |
| ...(options.headers || {}), | |
| }; | |
| const res = await fetch(url, { ...options, headers }); | |
| const text = await res.text(); | |
| let body; | |
| try { body = JSON.parse(text); } catch { body = text; } | |
| return { status: res.status, ok: res.ok, body }; | |
| async function relayFetch(path, options = {}) { | |
| const fetch = globalThis.fetch || (await import('node-fetch')).default; | |
| const url = `${RELAY_URL}${path}`; | |
| const headers = { | |
| 'Content-Type': 'application/json', | |
| ...(RELAY_TOKEN ? { Authorization: `Bearer ${RELAY_TOKEN}` } : {}), | |
| ...(options.headers || {}), | |
| }; | |
| const controller = new AbortController(); | |
| const timeout = setTimeout(() => controller.abort(), 30000); | |
| let res; | |
| let text; | |
| try { | |
| res = await fetch(url, { ...options, headers, signal: controller.signal }); | |
| text = await res.text(); | |
| } finally { | |
| clearTimeout(timeout); | |
| } | |
| let body; | |
| try { body = JSON.parse(text); } catch { body = text; } | |
| return { status: res.status, ok: res.ok, body }; | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/mcp/src/index.js` around lines 39 - 51, The relayFetch helper
currently waits on fetch with no timeout, so slow or half-open relay responses
can hang requests indefinitely. Update relayFetch to enforce a finite timeout
around the fetch call, using an AbortController or equivalent timeout mechanism,
and make sure the timeout is applied to the fetch invocation while preserving
the existing headers, response parsing, and returned { status, ok, body } shape.
| inputSchema: { | ||
| type: 'object', | ||
| required: ['provider', 'path', 'body'], | ||
| properties: { | ||
| provider: { | ||
| type: 'string', | ||
| enum: ['openai', 'anthropic', 'google', 'mistral', 'openai-compatible'], | ||
| description: 'AI provider to route to.', | ||
| }, | ||
| path: { | ||
| type: 'string', | ||
| description: 'Provider API path (e.g. "/v1/chat/completions" for OpenAI).', | ||
| }, | ||
| body: { | ||
| type: 'object', | ||
| description: 'Request body as a JSON object (will be forwarded verbatim).', | ||
| }, | ||
| method: { | ||
| type: 'string', | ||
| enum: ['POST', 'GET', 'DELETE'], | ||
| description: 'HTTP method. Default: POST.', | ||
| }, | ||
| }, |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
byok_relay_request can't express documented provider calls.
The relay API depends on passthrough headers such as anthropic-version and x-relay-base-url, but this tool exposes no headers field and still marks body as required even for GET. Anthropic and openai-compatible requests therefore cannot be represented from MCP despite being documented relay flows.
Suggested fix
inputSchema: {
type: 'object',
- required: ['provider', 'path', 'body'],
+ required: ['provider', 'path'],
properties: {
provider: {
type: 'string',
enum: ['openai', 'anthropic', 'google', 'mistral', 'openai-compatible'],
description: 'AI provider to route to.',
@@
body: {
type: 'object',
description: 'Request body as a JSON object (will be forwarded verbatim).',
},
+ headers: {
+ type: 'object',
+ description: 'Additional relay/provider headers to forward.',
+ additionalProperties: { type: 'string' },
+ },
method: {
type: 'string',
enum: ['POST', 'GET', 'DELETE'],
description: 'HTTP method. Default: POST.',
},
@@
const method = args.method || 'POST';
const relayPath = `/relay/${args.provider}${args.path.startsWith('/') ? args.path : '/' + args.path}`;
const fetchOpts = {
method,
- ...(method !== 'GET' && args.body ? { body: JSON.stringify(args.body) } : {}),
+ ...(args.headers ? { headers: args.headers } : {}),
+ ...(method !== 'GET' && args.body != null ? { body: JSON.stringify(args.body) } : {}),
};Also applies to: 260-266
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/mcp/src/index.js` around lines 124 - 146, The byok_relay_request
schema in inputSchema is missing support for the passthrough headers required by
documented relay calls, so add a headers field and wire it through the relay
request handling. Also make body optional for non-POST methods in the schema and
validation logic, since GET requests should not require it. Update the
byok_relay_request path in packages/mcp/src/index.js so Anthropic and
openai-compatible flows can express the documented relay calls using the
existing provider, path, and method fields plus the new headers support.
| } catch { | ||
| res.status(404).json({ error: 'OpenAPI spec not found' }); |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Don’t collapse every OpenAPI load failure into a 404.
These catch blocks also hide parse errors and serialization failures as “not found”, which makes runtime debugging much harder and returns the wrong status to clients. Please return 404 only for missing-file cases and surface other failures as 500s.
Also applies to: 157-158
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/index.js` around lines 141 - 142, Update the OpenAPI load handlers in the
relevant route logic so the `catch` blocks no longer always return 404 for
`OpenAPI spec not found`; use the existing loader/response path to distinguish
missing-file errors from parse or serialization failures. In the handler(s)
around the OpenAPI spec lookup, return 404 only when the failure is specifically
“file not found,” and return a 500 with an appropriate error response for all
other exceptions. Apply the same fix to both affected catch sites so the
behavior is consistent.
Summary
Adds
packages/mcp/— a complete MCP server (@byok-relay/mcp) that exposes byok-relay as 6 tools for Claude Desktop, Claude Code, Cursor, Windsurf, and any MCP-compatible client.Why
The MCP ecosystem is growing fast. Every Claude Desktop and Claude Code user is a potential byok-relay user — an MCP server gets byok-relay in front of them at the moment they need BYOK AI in a frontend app, with zero friction (
npx -y @byok-relay/mcp, no install step). Once listed on mcp.so and in the Anthropic MCP registry, it becomes a self-sustaining discovery channel.Tools exposed
byok_relay_healthbyok_relay_registerbyok_relay_store_keybyok_relay_requestbyok_relay_chatbyok_relay_statsClaude Desktop setup (zero-install)
{ "mcpServers": { "byok-relay": { "command": "npx", "args": ["-y", "@byok-relay/mcp"], "env": { "RELAY_URL": "https://relay.byokrelay.com", "RELAY_TOKEN": "<your-relay-token>" } } } }Other changes
llms.txt: MCP Server section + npm link so AI agents discover the MCP pathREADME.md: MCP badge in header + "Use from Claude Desktop" section before Deploypackage.json: workspaces includespackages/*(alongside existingpackages/client)Metrics
stars=51 forks=0 views=17 clones=116 (2026-06-29)
Next step (Avi)
Once merged: publish
@byok-relay/mcpto npm withcd packages/mcp && npm publish --access public, then submit to mcp.so and Anthropic MCP registry.Summary by CodeRabbit
New Features
Documentation