feat(mcp): add hosted httpSMS server - #990
Conversation
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add delegated MCP-to-API JWT authentication: a route-, scope-, and subject-bound token verifier (pkg/auth) and a Fiber middleware (MCPDelegationAuth) that loads the existing user auth context from the token's Firebase-UID subject when the token is valid for the exact method/path/scope of the request. MCPDelegationAuth is registered before BearerAuth so a cryptographically valid but insufficiently-scoped or misbound MCP token is rejected with 403 directly, instead of falling through to Firebase ID token verification. BearerAuth now short-circuits when a prior middleware has already populated the auth context, and no longer logs the raw bearer token on verification failure. The verifier is wired into the DI container from MCP_AUTH_ISSUER, MCP_AUTH_AUDIENCE, and MCP_AUTH_JWKS_URL; it is disabled when all three are empty and container construction fails fast when only some are set. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 9ff1e38a-b018-4cf7-a5e9-5044a2efd03c
Add GET /v1/messages/incoming, scoped to messages:read, that reuses MessageService.SearchMessages while forcing types=[mobile-originated]. The endpoint has no CAPTCHA requirement, unlike /v1/messages/search which remains unchanged. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 9ff1e38a-b018-4cf7-a5e9-5044a2efd03c
Bootstrap the mcp/ Go module: validated config.Load(), RSA KeySet signing/JWKS (auth.NewKeySet/SignMCPAccessToken/SignAPIDelegationToken/ JWKS), and an observability.New() logging/tracing bootstrap. API delegation tokens carry JSON fields scopes, http_method, and http_path, matching api/pkg/auth.MCPClaims exactly. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 9ff1e38a-b018-4cf7-a5e9-5044a2efd03c
Review round 1 fixes for Task 3: - Remove the unused firebase.google.com/go dependency and its entire transitive graph. It pulled a Go 1.26 floor via a transitive dep while this module must stay on Go 1.25; the approved Task 5 design uses a custom Firebase certificate/JWT verifier instead of the Admin SDK. go.mod remains `go 1.25.0`; the still-needed-later pins (modelcontextprotocol/go-sdk v1.7.0, redis/go-redis/v9, otelhttp) are preserved per the multi-task plan. - Replace KeySet's exported, freely-mutable Issuer/MCPAudience/APIAudience fields with a private atomic.Pointer[keySetConfig] published exactly once via a new Configure(issuer, mcpAudience, apiAudience string) error. Configure rejects empty values and a second call; signing methods fail closed until Configure has succeeded. Publishing the whole config behind a single CompareAndSwap (rather than a bool flag written before the fields) avoids a visibility race where a reader could see "configured" before the fields were set. - Add tests: unconfigured signing rejected, successful configuration, reconfiguration rejection (with slot-not-consumed-by-invalid-call and original-values-preserved checks), and a concurrent-Configure-calls test. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 9ff1e38a-b018-4cf7-a5e9-5044a2efd03c
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 9ff1e38a-b018-4cf7-a5e9-5044a2efd03c
- Pin the CIMD document fetch's actual TCP connection to the single IP address already validated as public (resolve exactly once), instead of letting the http.Transport dial its own second, unvalidated DNS resolution of the client_id host. Closes a DNS-rebinding/TOCTOU gap. Host header and TLS SNI still use the original hostname since only the dial address changes. - Reject CIMD responses whose Content-Type is not application/json (charset and other parameters are still allowed), wrapping ErrClientMetadataInvalid. - NewRedisStore now panics for a *redis.ClusterClient or *redis.Ring: RotateRefreshToken's Lua script touches two independently-hashed keys in one atomic EVAL, which the approved key format cannot guarantee share a Redis Cluster hash slot. This service requires a standalone Redis client (redis.NewClient); documented in RedisStore. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 9ff1e38a-b018-4cf7-a5e9-5044a2efd03c
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 9ff1e38a-b018-4cf7-a5e9-5044a2efd03c
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 9ff1e38a-b018-4cf7-a5e9-5044a2efd03c
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 9ff1e38a-b018-4cf7-a5e9-5044a2efd03c
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 9ff1e38a-b018-4cf7-a5e9-5044a2efd03c
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 9ff1e38a-b018-4cf7-a5e9-5044a2efd03c
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 9ff1e38a-b018-4cf7-a5e9-5044a2efd03c
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 9ff1e38a-b018-4cf7-a5e9-5044a2efd03c
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 9ff1e38a-b018-4cf7-a5e9-5044a2efd03c
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 9ff1e38a-b018-4cf7-a5e9-5044a2efd03c
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 9ff1e38a-b018-4cf7-a5e9-5044a2efd03c
Add multi-stage Dockerfile (Go 1.25 alpine builder, non-root alpine 3.22 runtime with ca-certificates/tzdata), cloudbuild.yaml mirroring api/cloudbuild.yaml for the http-sms-mcp Cloud Run service, .dockerignore, and operator README covering env vars, local startup, health/MCP routes, Cloud Build invocation, one-time mcp.httpsms.com domain mapping, Firebase authorized-domain setup, API coordination, signing-key rotation, standalone-Redis constraint, 2025-11-25 removal plan, and secret/redaction rules. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 9ff1e38a-b018-4cf7-a5e9-5044a2efd03c
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 9ff1e38a-b018-4cf7-a5e9-5044a2efd03c
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 9ff1e38a-b018-4cf7-a5e9-5044a2efd03c
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 9ff1e38a-b018-4cf7-a5e9-5044a2efd03c
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 9ff1e38a-b018-4cf7-a5e9-5044a2efd03c
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 9ff1e38a-b018-4cf7-a5e9-5044a2efd03c
# Conflicts: # tests/README.md # tests/docker-compose.yml
Delegated MCP verification now prefilters bearer tokens on their unverified "iss" claim -- parsed only to discard tokens that cannot be ours and never trusted for authentication -- so Firebase ID tokens and other non-MCP credentials seen by the pre-BearerAuth middleware never reach the JWKS cache. The JWKS cache itself collapses concurrent refreshes into one in-flight fetch, refuses a new fetch until MinRefreshInterval (default one minute) has elapsed, and keeps serving an already known key while throttled, mirroring the MCP Firebase certificate cache. The 2s HTTP timeout, key rotation, and fail-open middleware behavior are unchanged. The MCP CIMD client cache is bounded at 1024 entries, purging expired entries and then deterministically evicting the entry closest to expiring under the existing mutex, preserving its 15-minute TTL. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 9ff1e38a-b018-4cf7-a5e9-5044a2efd03c
|
| GitGuardian id | GitGuardian status | Secret | Commit | Filename | |
|---|---|---|---|---|---|
| 36901872 | Triggered | RSA Private Key | bf06243 | mcp/internal/config/config_test.go | View secret |
| 36901871 | Triggered | Generic High Entropy Secret | a13bc7d | mcp/internal/tools/api_keys_test.go | View secret |
🛠 Guidelines to remediate hardcoded secrets
- Understand the implications of revoking this secret by investigating where it is used in your code.
- Replace and store your secrets safely. Learn here the best practices.
- Revoke and rotate these secrets.
- If possible, rewrite git history. Rewriting git history is not a trivial act. You might completely break other contributing developers' workflow and you risk accidentally deleting legitimate data.
To avoid such incidents in the future consider
- following these best practices for managing and storing secrets including API keys and other credentials
- install secret detection on pre-commit to catch secret before it leaves your machine and ease remediation.
🦉 GitGuardian detects secrets in your source code to help developers and security teams secure the modern development process. You are seeing this because you or someone else with access to this repository has authorized GitGuardian to scan your pull request.
Not up to standards ⛔🔴 Issues
|
| Category | Results |
|---|---|
| Security | 2 minor 23 high 13 critical 22 medium |
| CodeStyle | 40 minor |
🟢 Metrics 1762 complexity · 768 duplication
Metric Results Complexity 1762 Duplication 768
NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.
| } | ||
| req.Header.Set("Accept", "application/json") | ||
|
|
||
| resp, err := r.httpClient.Do(req) |
| func (s *Server) redirect(w http.ResponseWriter, r *http.Request, target string) { | ||
| w.Header().Set("Cache-Control", "no-store") | ||
| w.Header().Set("Pragma", "no-cache") | ||
| http.Redirect(w, r, target, http.StatusFound) |
Greptile SummaryThe PR adds a standalone, stateless MCP service with Firebase-backed OAuth, scoped delegated JWT authentication, Redis state and rate limits, seven httpSMS tools, and a dedicated incoming-message API.
Confidence Score: 4/5The PR is not ready to merge because the checked-in production deployment leaves API-side MCP delegation disabled, causing all hosted MCP tool calls to fail authorization. The MCP service correctly mints delegated API tokens, but the production API deployment supplies none of the trust settings required to install their verifier, so those tokens cannot authenticate against api.httpsms.com. Files Needing Attention: api/pkg/di/config.go, api/pkg/di/container.go, api/cloudbuild.yaml Important Files Changed
Sequence DiagramsequenceDiagram
participant Client as MCP Client
participant MCP as Hosted MCP Server
participant Firebase as Firebase
participant Redis as Redis
participant API as httpSMS API
Client->>MCP: OAuth authorization + PKCE
MCP->>Firebase: Verify identity token
MCP->>Redis: Store/consume grant state
MCP-->>Client: MCP access + refresh tokens
Client->>MCP: Authenticated tool call
MCP->>MCP: Check scope and mint operation-bound JWT
MCP->>API: HTTP request with delegated JWT
API->>MCP: User-scoped result
MCP-->>Client: Structured tool result
Reviews (1): Last reviewed commit: "fix(auth): bound token metadata caches" | Re-trigger Greptile |
| if issuer == "" && audience == "" && jwksURL == "" { | ||
| return auth.MCPTokenVerifierConfig{}, false, nil | ||
| } |
There was a problem hiding this comment.
Summary
2026-07-28and2025-11-25compatibilityTools
list_phonessend_smslist_message_threadslist_thread_messageslist_incoming_messagescreate_phone_api_keyrotate_user_api_keyValidation