feat(mcp): resolve per-tenant endpoint with shared-URL fallback#154
Draft
aisling404 wants to merge 3 commits into
Draft
feat(mcp): resolve per-tenant endpoint with shared-URL fallback#154aisling404 wants to merge 3 commits into
aisling404 wants to merge 3 commits into
Conversation
On first run, provision an isolated MCP endpoint on Telnyx Edge Compute (POST /v2/compute/mcp/deploy) and cache the URL at ~/.telnyx/mcp-endpoint.json keyed by a sha256 fingerprint of the API key. If provisioning is unavailable (404/5xx/network/timeout), fall back transparently to the shared hosted endpoint so connections keep working. TELNYX_MCP_URL overrides resolution; --reset / TELNYX_MCP_RESET discards the cached endpoint. Bumps @telnyx/mcp to 0.2.0. Plugin manifests are unchanged — once this is on npm, a follow-up PR will flip providers/claude, providers/cursor, and gemini-extension.json from httpUrl to spawning the shim so users auto-get per-tenant URLs without reconfiguring.
Align the shim with Xingda's deploy-API contract: POST requires a pre-created api_key_secret_id. The shim now ensures a secret exists via a deterministic-name lookup (GET /v2/compute/secrets?filter[name]=…) and creates one if missing, then calls deploy with the returned ID. Gate per-account provisioning behind TELNYX_MCP_ACCEPT_FULL_SCOPE. The injected secret is the user's raw Telnyx API key, and VM-internal compromise inside the MCP func (Deno eval in Code Mode) would leak it full-scope. Without the consent env var, the shim stays on the shared hosted endpoint — existing user behavior. README documents the trade and flags the scoped-credential follow-up as a v2 hardening. ResolveResult.source split into fallback-no-consent vs fallback-error so CLI can surface the right nudge (opt-in vs transient failure).
| } | ||
|
|
||
| function fingerprint(apiKey: string): string { | ||
| return createHash("sha256").update(apiKey).digest("hex").slice(0, 16); |
Per the team-telnyx/edge-compute reference example, deployed MCP funcs
require a SHARED_SECRET distinct from TELNYX_API_KEY:
- TELNYX_API_KEY: used by the func to call Telnyx APIs upstream
- SHARED_SECRET: bearer token clients send inbound to the func
Without inbound auth a deployed Code Mode MCP exposes the upstream
TELNYX_API_KEY to anyone who can reach the URL — the example returns
503 unless SHARED_SECRET is set.
The shim now:
- Expects the deploy API to generate SHARED_SECRET server-side and
return it in the response (cleaner than client-side generation
given that Telnyx secrets list returns names only — values are
unrecoverable, so server-managed avoids dual-source-of-truth)
- Caches the value alongside the per-tenant URL at
~/.telnyx/mcp-endpoint.json (mode 0600)
- Returns a remoteAuthToken alongside the URL so callers don't have
to track which bearer goes with which endpoint
- Uses shared_secret for per-tenant URLs; falls back to api_key for
the shared hosted URL (today's behavior)
createProxy now takes remoteAuthToken explicitly and no longer
requires apiKey — the proxy doesn't need to know about the API key,
just which bearer to send to the resolved endpoint.
Validated via probing: /v2/compute/funcs and /v2/compute/secrets both
return 401 (exist), /v2/compute/mcp/deploy returns 404 (Xingda's
proposed endpoint is genuinely net-new). Secrets are org-scoped and
upsert by name per the public CLI docs. Telnyx API key auth is
supported by the Edge Compute control plane (auth api-key set).
Tests: 17 passing (added shared_secret in deploy response, missing
shared_secret as failure, remoteAuthToken assertions per source).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this PR does
@telnyx/mcpbecomes a small resolver-then-proxy:secrets add.POST /v2/compute/mcp/deploywithapi_key_secret_id. Cache the returned per-tenant URL and the server-generatedSHARED_SECRETat~/.telnyx/mcp-endpoint.json(mode 0600).SHARED_SECRETas the inbound bearer.If any step fails, or the user hasn't opted in, the shim transparently falls back to today's shared hosted endpoint
https://api.telnyx.com/v2/mcpusing the API key as bearer — current behavior preserved exactly.What's NOT in this PR (deliberately)
providers/claude/,providers/cursor/, orgemini-extension.json— those wait until@telnyx/mcp@0.2.0is on npm./v2/compute/mcp/deploy) from any production code path. The path 404s today; the shim swallows that and falls back.Background
Two distinct credentials — explicit for the first time
The reference example surfaces a split that the original deployment doc didn't fully spell out. This PR encodes it:
TELNYX_API_KEYapi_key_secret_idSHARED_SECRETServer-generation of
SHARED_SECRETis intentional. Telnyxsecrets listreturns names only — values are unrecoverable from the API once written. A client-generated value stored as a Telnyx secret would be lost forever on local cache loss. Server-managed avoids the dual-source-of-truth and keeps recovery simple: cache loss →--reset→ new value.Consent gate
Provisioning is opt-in via
TELNYX_MCP_ACCEPT_FULL_SCOPE=1. The injected upstream secret is the user's raw, full-scope Telnyx API key — VM-internal compromise inside the Code Mode MCP func (Deno eval on agent-written TS,--allow-envunrestricted) would exfiltrate it. Firecracker protects cluster blast radius but not account blast radius. Without the env var, the shim stays on the shared URL.v2 hardening candidate: swap raw API key for a scoped, short-lived credential. Tracked as open question 5 below; not blocking.
Validated against the live API (read-only probing)
GET /v2/compute/funcsGET /v2/compute/secretsGET /v2/compute/mcp/deployGET /v2/compute/funcs/{id}/secretsPublic docs further confirm: secrets are org-scoped,
secrets addis upsert by name, secret values are never returned via list, Telnyx API key auth IS supported by the Edge Compute control plane (auth api-key set), per-tenant URL pattern ishttps://{funcName}-{orgId}.telnyxcompute.com.Tests
npm test— 17/17 passing. Coverage:TELNYX_MCP_URL) wins, uses API key as bearerTELNYX_MCP_ACCEPT_FULL_SCOPEunset /0/falseall skip provisioning, return shared URL)api_key_secret_id)url/func_id/shared_secretin response, network error, abort)SHARED_SECRETreturned without any fetch calls)--reset+TELNYX_MCP_RESETenv var)remoteAuthTokenassertions across every source: API key for shared/override/no-consent,SHARED_SECRETfor cache/provisionedOpen questions for Edge Compute team
The shim is built against best-guess shapes for the new
mcp/deployendpoint. Each unconfirmed assumption below has a fallback path that keeps users on the shared URL — wrong guesses don't break anything, they just leave per-tenant URLs unused. Still want explicit answers before flipping plugin manifests:POST /v2/compute/mcp/deployidempotent bynamewithin an org? Shim calls deploy on every cache miss and assumes "yes."{ data: { url, func_id, shared_secret, status, ... } }. Confirm field names and JSON envelope.shared_secret— confirmed approach? Alternative is shim-generated; server-managed is cleaner per the unrecoverability constraint.res.ok === falseas failure. If the endpoint can return202for long provisioning with a separate poll URL, first-run users silently stay on shared URL until we add polling logic.TELNYX_API_KEYinstead of the user's raw key?SHARED_SECRETrotation — separate endpoint to rotate without redeploying, or is--resetthe only path?Sequencing
@telnyx/mcp@0.2.0via manualworkflow_dispatchonpublish-npm.ymlFiles changed
tools/mcp/only:src/endpoint.ts(new) — resolver, secret ensure, deploy call, cachesrc/index.ts—createProxytakesremoteAuthTokeninstead ofapiKey; drops hardcoded URL constantsrc/cli.ts— wires resolver to proxy, expanded help texttests/endpoint.test.ts(new) — 17 casespackage.json— bump to 0.2.0, addtest+typecheckscripts, addtsxdevDeppackage-lock.json— tsxREADME.md— documents dual-credential model and opt-in flow🤖 Generated with Claude Code