Skip to content

[ENG-623] feat: remote mcp#150

Open
miguelangaranocurrents wants to merge 4 commits into
mainfrom
feat/remote-mcp
Open

[ENG-623] feat: remote mcp#150
miguelangaranocurrents wants to merge 4 commits into
mainfrom
feat/remote-mcp

Conversation

@miguelangaranocurrents

@miguelangaranocurrents miguelangaranocurrents commented Jun 16, 2026

Copy link
Copy Markdown
Collaborator

Remote MCP HTTP server + container publish

Summary

Adds a Streamable HTTP transport so the Currents MCP server can run as a hosted remote endpoint (e.g. for Claude web/mobile connectors), while keeping the existing stdio transport unchanged for local clients.

The same Publish NPM Package workflow now also builds and pushes a Docker image to GHCR. For beta and latest channels, a successful push triggers deployment in the private infrastructure repo via repository_dispatch.

Motivation

Today the server only speaks stdio — fine for Claude Desktop and Cursor, but remote connectors need an HTTPS URL. The goal is one package, one set of tools, two entry points: stdio for local use, HTTP for hosted use.

Authentication for the hosted path is intentionally not implemented in the MCP server. Callers pass their Currents API key as a Bearer token; the server forwards it to the Currents API, which remains the sole auth authority.

Architecture

flowchart TD
  stdio["stdio entry"] -->|"CURRENTS_API_KEY env"| ctx[requestContext]
  http["HTTP entry"] -->|"Authorization Bearer header"| ctx
  ctx --> req["getApiKey in request.ts"]
  req --> tools["tool handlers unchanged"]
  tools --> api["Currents API"]
Loading
Transport Entry API key
stdio dist/index.mjs CURRENTS_API_KEY env var (required at startup)
HTTP dist/http.mjs Authorization: Bearer <key> per request

Key design decisions

Per-request context via AsyncLocalStorage — The HTTP server is multi-tenant: concurrent requests may carry different API keys. Rather than threading a key through every tool handler, getApiKey() in request.ts reads from request-scoped context (HTTP) or falls back to the env var (stdio). Tool handlers and their signatures are untouched.

Stateless HTTP — Each POST /mcp creates a fresh McpServer + StreamableHTTPServerTransport with sessionIdGenerator: undefined. No session state, no cross-request key bleed. GET/DELETE on /mcp return 405.

Server factorycreateMcpServer() replaces the module-level singleton so both transports can instantiate servers independently.

What to review

Runtime (mcp-server/src/)

File Review focus
lib/context.ts ALS store shape; env fallback for stdio
lib/request.ts Four auth header sites now call getApiKey()
server.ts Factory extraction; stdio path still gated on env key
http.ts Stateless transport wiring, Bearer passthrough, /healthz, error handling

Packaging and Docker

File Review focus
package.json New start:http script and currents-mcp-http bin; stdio bin unchanged
tsdown.config.ts http build entry
Dockerfile Now runs HTTP server; CURRENTS_API_URL fixed to include /v1; no baked-in API key
.dockerignore Build context trimming

CI/CD (.github/workflows/publish.yaml)

New publish-image job runs after npm publish:

  1. Build + push ghcr.io/currents-dev/currents-mcp:<version> and :<channel>
  2. For beta / latest only: resolve GHCR digest, fire repository_dispatch (mcp-image-published) to currents-dev/currents

Worth checking: dispatch runs after push (digest must exist), failures are not swallowed, and alpha / semver-only publishes do not dispatch.

Tests

  • context.test.ts — concurrent ALS isolation
  • http.test.ts — Bearer parsing and outbound auth header passthrough
  • server.test.ts — updated to call factory explicitly
  • Existing suite should pass unchanged

Docs

  • README.md — remote endpoint usage for end users (client config, Bearer auth, local run)
  • .github/workflows/README.md — GHCR publish + DISPATCH_TOKEN secret

Backward compatibility

  • stdio clients (npx @currents/mcp, Claude Desktop config) are unaffected
  • All MCP tool names, schemas, and handlers are unchanged
  • npm publish channels and the Create Release workflow are unchanged
  • No AWS or infra config added to this repo

Test plan

# Unit tests
cd mcp-server && npm ci && npm run test:run

# HTTP locally
npm run build && PORT=3000 npm run start:http
curl http://localhost:3000/healthz

curl -s -N -X POST http://localhost:3000/mcp \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -H "Authorization: Bearer $CURRENTS_API_KEY" \
  -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"test","version":"0"}}}'
# expect serverInfo.name == "currents"

# Docker
docker build -t currents-mcp . && docker run --rm -p 3000:3000 currents-mcp

# stdio regression
CURRENTS_API_KEY=$CURRENTS_API_KEY node dist/index.mjs

After merge, before first beta/latest publish:

  1. Add DISPATCH_TOKEN repo secret (fine-grained PAT, Contents read/write on currents-dev/currents)
  2. Confirm Actions workflow permissions allow GITHUB_TOKEN to write packages

Deploy integration

GHCR channel tag Downstream action
latest Production deploy
beta Staging deploy
alpha, semver, oldversion Image published only; no auto-deploy

Dispatch payload: { "tag": "<channel>", "digest": "sha256:..." }.

Summary by CodeRabbit

  • New Features
    • Added a remote HTTP MCP endpoint with GET /healthz and POST /mcp support, using Bearer token authorization.
    • Introduced a new currents-mcp-http executable and start:http command.
  • Documentation
    • Documented the hosted HTTP endpoint usage and behavior in the README.
    • Updated CI workflow documentation for publishing the package and container.
  • Tests
    • Added coverage for API key extraction and per-request key propagation.
  • Refactor
    • Updated the server startup to create a fresh MCP server instance with tool registration.
  • Chores
    • Improved Docker defaults and added a comprehensive .dockerignore.

@coderabbitai

coderabbitai Bot commented Jun 16, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: b392671c-41fa-4d26-a102-c23dc02a38dc

📥 Commits

Reviewing files that changed from the base of the PR and between 9a997cc and a47ce87.

📒 Files selected for processing (1)
  • .github/workflows/publish.yaml
💤 Files with no reviewable changes (1)
  • .github/workflows/publish.yaml

📝 Walkthrough

Walkthrough

Adds a stateless hosted HTTP transport for the MCP server (http.ts) with per-request Bearer-token API key propagation via AsyncLocalStorage (context.ts). Refactors McpServer construction into an exported createMcpServer() factory. Updates the Dockerfile to use the HTTP entrypoint, adds .dockerignore, expands build config and package.json scripts, and adds a CI publish-image job to build and push the image to GHCR with downstream dispatch.

Changes

HTTP MCP Transport and Publish Pipeline

Layer / File(s) Summary
Per-request API key context and request helper wiring
mcp-server/src/lib/context.ts, mcp-server/src/lib/context.test.ts, mcp-server/src/lib/request.ts
RequestContext interface and AsyncLocalStorage store provide per-request API key isolation; getApiKey() falls back to the env var when no context is active; fetchApi, postApi, putApi, and deleteApi are updated to call getApiKey() instead of reading CURRENTS_API_KEY statically; concurrency and fallback behavior are tested.
McpServer factory and HTTP entrypoint
mcp-server/src/server.ts, mcp-server/src/http.ts, mcp-server/src/http.test.ts, mcp-server/src/server.test.ts
createMcpServer() factory replaces the module-level singleton; http.ts implements extractApiKey, stateless per-request handleMcpPost (creates a fresh server and StreamableHTTPServerTransport, runs within requestContext), createHttpServer routing (GET /healthz, POST /mcp, 404/405 fallbacks), and a start() function; tests cover Bearer-token extraction, request isolation, and concurrent key non-bleed.
Build config, packaging, and Dockerfile
mcp-server/tsdown.config.ts, mcp-server/package.json, Dockerfile, .dockerignore
http.ts is added as a tsdown entry; package.json exposes the currents-mcp-http binary and start:http script; Dockerfile switches entrypoint to dist/http.mjs, sets PORT=3000, EXPOSE 3000, and updates CURRENTS_API_URL to the /v1 path; .dockerignore excludes build artifacts, editor metadata, and test directories.
GHCR image publish CI job and docs
.github/workflows/publish.yaml, .github/workflows/README.md, README.md
Adds job-scoped packages: write permission; a new publish-image job builds and pushes the Docker image to ghcr.io with version+channel tags, resolves the manifest digest via GHCR API, and fires a mcp-image-published repository dispatch for latest/beta channels; workflow README and root README document the publish pipeline and hosted HTTP endpoint usage.

Sequence Diagram(s)

sequenceDiagram
  participant Client as MCP Client
  participant createHttpServer as createHttpServer
  participant handleMcpPost as handleMcpPost
  participant requestContext as requestContext (AsyncLocalStorage)
  participant createMcpServer as createMcpServer
  participant fetchApi as fetchApi / Currents API

  Client->>createHttpServer: POST /mcp (Authorization: Bearer <apiKey>)
  createHttpServer->>handleMcpPost: req, res
  handleMcpPost->>handleMcpPost: readJsonBody → parsed body
  handleMcpPost->>handleMcpPost: extractApiKey(req) → apiKey
  handleMcpPost->>createMcpServer: createMcpServer()
  handleMcpPost->>requestContext: run({ apiKey }, async () => ...)
  requestContext->>fetchApi: Authorization: Bearer <apiKey> (via getApiKey())
  fetchApi-->>requestContext: Currents API response
  requestContext-->>handleMcpPost: MCP response written to res
  handleMcpPost-->>Client: JSON-RPC response
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • currents-dev/currents-mcp#136: Both PRs modify mcp-server/tsdown.config.ts build configuration; this PR adds a new entry.http build target that extends the same build-definition infrastructure.
  • currents-dev/currents-mcp#133: Both PRs modify mcp-server/src/server.ts and mcp-server/src/server.test.ts around tool registration timing; this PR refactors registration into createMcpServer() while the related PR adds validation tests that depend on import-time behavior.
  • currents-dev/currents-mcp#134: Both PRs depend on the registerTool(...) entries in mcp-server/src/server.ts; this PR wraps them in the new createMcpServer() factory while the related PR validates them against README documentation.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 42.86% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title '[ENG-623] feat: remote mcp' clearly and concisely summarizes the main change: adding remote HTTP transport capabilities to the MCP server.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/remote-mcp

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (4)
mcp-server/src/http.ts (1)

24-31: 💤 Low value

Consider handling edge cases in Bearer token extraction.

The current implementation returns the raw header value for invalid Bearer formats (e.g., "Bearer" with no token returns "Bearer"). While this aligns with the design philosophy of forwarding tokens to the Currents API for validation, it may pass through malformed headers.

Consider either:

  • Returning undefined for invalid Bearer headers
  • Adding validation to ensure a non-empty token exists after "Bearer"
  • Documenting the intentional passthrough behavior
🛡️ Example validation approach
 export function extractApiKey(req: IncomingMessage): string | undefined {
   const auth = req.headers.authorization?.trim();
   if (!auth) {
     return undefined;
   }
   const match = /^Bearer\s+(.+)$/i.exec(auth);
-  return (match ? match[1] : auth).trim();
+  if (match) {
+    const token = match[1].trim();
+    return token || undefined; // reject empty tokens
+  }
+  // Accept raw tokens without Bearer prefix
+  return auth.trim() || undefined;
 }
🤖 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 `@mcp-server/src/http.ts` around lines 24 - 31, The extractApiKey function in
the Bearer token extraction logic does not validate that a valid token actually
exists after the "Bearer" prefix. Currently, malformed headers like "Bearer"
with no token are passed through as-is (returning "Bearer" itself). Update the
function to either return undefined when the Bearer prefix is present but no
token follows it, or add validation to ensure the matched group contains a
non-empty token before returning it. This prevents passing malformed
authentication headers downstream while maintaining the passthrough behavior for
non-Bearer authorization schemes.
README.md (1)

115-117: 💤 Low value

Add language specifier to fenced code block.

The code block showing the Authorization header format is missing a language specifier. While not critical, adding http or text improves rendering and satisfies linting rules.

📝 Suggested fix
-```
+```http
 Authorization: Bearer <your-currents-api-key>
</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @README.md around lines 115 - 117, The fenced code block displaying the
Authorization header format is missing a language specifier on the opening
backticks. Add a language identifier such as http or text immediately after
the opening triple backticks (change tohttp) to improve markdown
rendering and satisfy linting requirements.


</details>

<!-- cr-comment:v1:c93917ece7dab6edc19f8b7d -->

_Source: Linters/SAST tools_

</blockquote></details>
<details>
<summary>.github/workflows/publish.yaml (2)</summary><blockquote>

`102-104`: _⚡ Quick win_

**Consider disabling credential persistence for security.**

The checkout action does not set `persist-credentials: false`, allowing the GitHub token to persist in the repository's git config. If subsequent steps are compromised, the token could be misused.





<details>
<summary>🔒 Suggested hardening</summary>

```diff
       - uses: actions/checkout@v4
         with:
           ref: ${{ github.event.pull_request.head.sha || github.ref }}
+          persist-credentials: false
```
</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/publish.yaml around lines 102 - 104, Add the
`persist-credentials: false` parameter to the `with` section of the
actions/checkout@v4 action to prevent the GitHub token from persisting in the
git config. This ensures that if any subsequent workflow steps are compromised,
the token cannot be misused by an attacker.
```

</details>

<!-- cr-comment:v1:f2f5c547d3fbe8c377f2d297 -->

_Source: Linters/SAST tools_

---

`102-102`: _⚖️ Poor tradeoff_

**Consider pinning actions to commit SHAs for supply-chain security.**

The workflow uses semantic version tags (`@v3`, `@v4`, `@v5`) rather than immutable commit SHAs. While tags are more readable, they are mutable and could be compromised. Pinning to SHAs with a comment noting the version improves supply-chain security.





Example:
```yaml
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
```


Also applies to: 112-112, 114-114, 122-122, 129-129

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/publish.yaml at line 102, Replace all GitHub Actions
version tags with immutable commit SHAs for supply-chain security in the
publish.yaml workflow. In `.github/workflows/publish.yaml` at lines 102, 112,
114, 122, and 129, change each action reference from semantic version tags (such
as `@v4`, `@v3`, `@v5`) to specific commit SHAs by looking up the commit hash for each
action version and including the version as a comment for readability. For
example, replace `actions/checkout@v4` with the format
`actions/checkout@<commit-sha> # v4.1.1` where the commit SHA is the immutable
hash corresponding to that version tag.
```

</details>

<!-- cr-comment:v1:00b88ca52a278690646004a0 -->

_Source: Linters/SAST tools_

</blockquote></details>

</blockquote></details>

<details>
<summary>🤖 Prompt for all review comments with AI agents</summary>

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 @.github/workflows/publish.yaml:

  • Line 20: The packages: write permission at the workflow level (line 20)
    should be removed and kept only at the job level where it is actually needed.
    Delete the packages: write line from the workflow-level permissions section,
    since the publish-image job already correctly declares this permission at
    lines 97-99. This ensures the permission is scoped only to the job that requires
    it, adhering to the principle of least privilege.

Nitpick comments:
In @.github/workflows/publish.yaml:

  • Around line 102-104: Add the persist-credentials: false parameter to the
    with section of the actions/checkout@v4 action to prevent the GitHub token
    from persisting in the git config. This ensures that if any subsequent workflow
    steps are compromised, the token cannot be misused by an attacker.
  • Line 102: Replace all GitHub Actions version tags with immutable commit SHAs
    for supply-chain security in the publish.yaml workflow. In
    .github/workflows/publish.yaml at lines 102, 112, 114, 122, and 129, change
    each action reference from semantic version tags (such as @v4, @v3, @v5) to
    specific commit SHAs by looking up the commit hash for each action version and
    including the version as a comment for readability. For example, replace
    actions/checkout@v4 with the format actions/checkout@<commit-sha> # v4.1.1
    where the commit SHA is the immutable hash corresponding to that version tag.

In @mcp-server/src/http.ts:

  • Around line 24-31: The extractApiKey function in the Bearer token extraction
    logic does not validate that a valid token actually exists after the "Bearer"
    prefix. Currently, malformed headers like "Bearer" with no token are passed
    through as-is (returning "Bearer" itself). Update the function to either return
    undefined when the Bearer prefix is present but no token follows it, or add
    validation to ensure the matched group contains a non-empty token before
    returning it. This prevents passing malformed authentication headers downstream
    while maintaining the passthrough behavior for non-Bearer authorization schemes.

In @README.md:

  • Around line 115-117: The fenced code block displaying the Authorization header
    format is missing a language specifier on the opening backticks. Add a language
    identifier such as http or text immediately after the opening triple
    backticks (change tohttp) to improve markdown rendering and satisfy
    linting requirements.

</details>

<details>
<summary>🪄 Autofix (Beta)</summary>

Fix all unresolved CodeRabbit comments on this PR:

- [ ] <!-- {"checkboxId": "4b0d0e0a-96d7-4f10-b296-3a18ea78f0b9"} --> Push a commit to this branch (recommended)
- [ ] <!-- {"checkboxId": "ff5b1114-7d8c-49e6-8ac1-43f82af23a33"} --> Create a new PR with the fixes

</details>

---

<details>
<summary>ℹ️ Review info</summary>

<details>
<summary>⚙️ Run configuration</summary>

**Configuration used**: Organization UI

**Review profile**: CHILL

**Plan**: Pro

**Run ID**: `6b031673-0034-4f74-9c2d-18c9294ed71a`

</details>

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between e42e9fccd302f94942fdc6aac42ba5769a61008e and 9a997ccec09a30dc4b83690d5056b6098780be87.

</details>

<details>
<summary>⛔ Files ignored due to path filters (1)</summary>

* `mcp-server/package-lock.json` is excluded by `!**/package-lock.json`

</details>

<details>
<summary>📒 Files selected for processing (14)</summary>

* `.dockerignore`
* `.github/workflows/README.md`
* `.github/workflows/publish.yaml`
* `Dockerfile`
* `README.md`
* `mcp-server/package.json`
* `mcp-server/src/http.test.ts`
* `mcp-server/src/http.ts`
* `mcp-server/src/lib/context.test.ts`
* `mcp-server/src/lib/context.ts`
* `mcp-server/src/lib/request.ts`
* `mcp-server/src/server.test.ts`
* `mcp-server/src/server.ts`
* `mcp-server/tsdown.config.ts`

</details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->

Comment thread .github/workflows/publish.yaml Outdated
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants