Skip to content

feat: expose daemon-managed MCP servers via serve#172

Merged
steipete merged 3 commits into
openclaw:mainfrom
zm2231:feat/mcporter-serve
May 14, 2026
Merged

feat: expose daemon-managed MCP servers via serve#172
steipete merged 3 commits into
openclaw:mainfrom
zm2231:feat/mcporter-serve

Conversation

@zm2231
Copy link
Copy Markdown
Contributor

@zm2231 zm2231 commented May 14, 2026

Summary

Adds mcporter serve, a bridge that exposes daemon-managed keep-alive servers as a single MCP endpoint.

The mcporter daemon already keeps keep-alive servers warm between calls, but MCP clients such as Claude Code and Codex cannot connect to it directly because it speaks mcporter's private Unix-socket protocol. Without a bridge, each client session still launches its own copy of every configured MCP server.

mcporter serve sits between the daemon-backed runtime and any MCP client. Clients register one MCP server, while mcporter handles tool discovery, namespacing, routing, and lifecycle reuse through the daemon.

What's Included

  • src/serve.ts

    • MCP SDK McpServer bridge.
    • Accepts a Runtime surface (listTools / callTool) rather than a raw DaemonClient.
    • Exposes selected keep-alive tools as server__tool to avoid cross-server name collisions.
    • Escapes bridged server/tool name parts so configured names containing __ cannot collide.
    • Supports stdio and Streamable HTTP transports.
  • src/cli/serve-command.ts

    • Adds mcporter serve.
    • Wires createRuntime + DaemonClient + createKeepAliveRuntime.
    • Defaults to stdio.
    • Supports --http <port> on 127.0.0.1 by default.
    • Supports --host <host> for explicit non-local binding.
    • Supports --servers a,b,c to restrict exposed keep-alive servers.

Review Fixes

  • Rejects empty equals-form HTTP ports such as --http= instead of treating them as port 0.
  • Adds bridge coverage with a real createKeepAliveRuntime wrapper to prove served keep-alive tool listing/calls route through the daemon client and trigger restart/close behavior on fatal daemon errors.
  • Escapes bridged tool-name components to prevent collisions such as alpha + beta__ping and alpha__beta + ping.

Tests

Added coverage in:

  • tests/serve.test.ts
  • tests/cli-serve-command.test.ts
  • tests/cli-serve-runtime.test.ts

Validated with:

  • ./runner pnpm check
  • ./runner pnpm docs:list
  • ./runner pnpm exec tsx src/cli.ts serve --help
  • ./runner pnpm test

Result:

  • 116 test files passed, 1 skipped
  • 571 tests passed, 3 skipped

Forward Compatibility

If createManagedRuntime() lands later, only serve-command.ts needs to swap its manual runtime wiring for that helper. serve.ts should not need changes because it already depends on the runtime interface rather than daemon construction details.

@zm2231 zm2231 marked this pull request as ready for review May 14, 2026 00:33
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: bfe727150c

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/serve.ts Outdated
Comment on lines +165 to +166
export function encodeToolName(server: string, tool: string): string {
return `${server}${TOOL_SEPARATOR}${tool}`;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Escape bridged names before joining them

When two served keep-alive servers can be named with the separator (config keys are arbitrary strings), this encoding can produce duplicate exposed tool names: for example server alpha tool beta__ping and server alpha__beta tool ping both become alpha__beta__ping. Because decodeToolName uses longest-prefix matching, calls to the first tool are routed to alpha__beta instead, so one advertised tool becomes uncallable. Please reject/escape __ in server names or use an unambiguous encoding.

Useful? React with 👍 / 👎.

@steipete steipete merged commit 89f5053 into openclaw:main May 14, 2026
5 checks passed
@steipete
Copy link
Copy Markdown
Collaborator

Landed on main in 8d962fb.

I could not update this PR branch directly because maintainerCanModify is false, so I recreated the branch in the maintainer repo, added the required changelog entry and one fix to keep bridged names readable (memory__create_entities instead of percent-escaping ordinary underscores), then landed that tested branch.

Proof run locally before landing:

  • ./runner pnpm check
  • ./runner pnpm test
  • ./runner pnpm docs:list
  • manual mcporter serve --http 0 e2e against a keep-alive stdio memory fixture, including list/call through the bridge

Thanks @zm2231. For future PRs, enabling "Allow edits by maintainers" lets us push small fixups directly to the PR branch.

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