Skip to content

feat(mcp): single-agent default at session init#98

Merged
jiashuoz merged 1 commit into
mainfrom
feat/mcp-agent-prefetch
May 21, 2026
Merged

feat(mcp): single-agent default at session init#98
jiashuoz merged 1 commit into
mainfrom
feat/mcp-agent-prefetch

Conversation

@jiashuoz
Copy link
Copy Markdown
Member

Summary

End-to-end test against the freshly-deployed hosted MCP surfaced friction: every message-scoped tool (list_messages, get_message, send_email, reply_to_message, …) errored "agentEmail is required" when called without an explicit agent_email, even though the user had exactly one agent and had just authorized that agent at OAuth consent. Only whoami worked, because it carries its own inline listAgents() resolution.

This hoists whoami's resolution to session-init time. On isInitializeRequest, if the constructed E2AClient has no agentEmail (env var unset — the dominant hosted-OAuth case), the MCP server calls listAgents() once; if the user has exactly one agent, the client is rebuilt with that email pinned. Every subsequent tool call in the session sees a populated client.agentEmail and the SDK's per-call requireEmail() returns it instead of throwing.

Resolution order (highest precedence first)

  1. E2A_AGENT_EMAIL env var — constructor populates agentEmail; operator opted in, don't second-guess
  2. listAgents() yields exactly one agent → use it
  3. Otherwise leave empty — multi-agent users keep today's UX (pass explicitly or rely on whoami's resolution)

Scope of change

  • Zero backend change — agent_email already lives in oauth_access_tokens.request JSONB via oauth.Session, but exposing it requires a new endpoint. Deferred.
  • Zero schema change
  • Zero SDK changeE2AClient.agentEmail stays readonly; we just rebuild the instance with the resolved email
  • Pure MCP server change: 71 lines added in http-server.ts, 132 lines of tests in http.test.ts

Why not the OAuth-grant binding approach?

It's the right shape for multi-agent users (read session.AgentEmail from the bound grant — already persisted, just no API to expose it). But we have zero multi-agent OAuth users today, and that path needs a backend endpoint, auth-middleware refactor to thread session-on-context, and an MCP-side preflight call. YAGNI for the actual reported problem. Filed as a follow-up if a multi-agent OAuth user ever surfaces friction.

Failure handling

listAgents() errors are swallowed; session init proceeds with empty agentEmail. A transient backend hiccup shouldn't break MCP initialize — worst case the user sees the same "agentEmail is required" error they'd see today.

Test plan

  • Unit: resolves agent_email when listAgents returns exactly one agent (factory called twice, second call carries the resolved email)
  • Unit: skips the listAgents probe when agentEmail is already set (env var path; factory called once)
  • Unit: leaves agentEmail empty when the account has multiple agents (factory called once, no rebuild)
  • Unit: does not block session init when listAgents throws (initialize still completes, tools listable)
  • All 84 existing MCP tests still pass
  • npx tsc --noEmit clean
  • After merge + npm publish (mcp-v0.3.1) + prod deploy: end-to-end smoke against hosted mcp.e2a.devlist_messages succeeds without explicit agent_email

🤖 Generated with Claude Code

Test pass against hosted mcp.e2a.dev surfaced friction: every
message-scoped tool (list_messages, get_message, send_email, …)
errored "agentEmail is required" when called without an explicit
agent_email, even though the user had exactly one agent and had
just authorized that agent at OAuth consent. Only `whoami` worked,
because it carries its own inline listAgents() resolution.

The fix hoists whoami's resolution to session-init time. On
isInitializeRequest, if the constructed E2AClient has no agentEmail
(env var unset — the dominant hosted-OAuth case), call listAgents()
once; if the user has exactly one agent, rebuild the client with
that email pinned. Every subsequent tool call in the session sees
a populated client.agentEmail and the SDK's per-call requireEmail()
returns it instead of throwing.

Resolution order (highest precedence first):
  1. E2A_AGENT_EMAIL env var (constructor reads it)
  2. listAgents() yields exactly one → use it
  3. Otherwise leave empty — multi-agent users keep today's UX
     (pass explicitly or rely on whoami's resolution)

Failure handling: listAgents() errors are swallowed, session init
proceeds with empty agentEmail. A transient backend hiccup
shouldn't break MCP initialize; worst case the user sees the same
error they'd see today.

clientFactory signature extended with an optional second arg
({ agentEmail }) so tests can observe the resolved email round-
tripping back into the second client construction. Existing tests
that pass a 0-arg factory keep working — the helper only passes
the second arg when an email actually got resolved.

This is the cheapest viable fix: zero backend change, zero schema
change, zero SDK change. A future slice can read session.AgentEmail
from the OAuth grant directly (already persisted in
oauth_access_tokens.request JSONB) to disambiguate multi-agent
accounts — but we have none today.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@jiashuoz jiashuoz merged commit 187562a into main May 21, 2026
10 checks passed
@jiashuoz jiashuoz deleted the feat/mcp-agent-prefetch branch May 21, 2026 23:19
jiashuoz added a commit that referenced this pull request May 22, 2026
…ts (#99)

* docs(examples): add hosted-MCP variant to ADK, LangChain, OpenAI Agents

Each framework example now ships two scripts that exercise the same
e2a tool surface:

  agent.py         — stdio: npx -y @e2a/mcp-server, local Node child
  agent_hosted.py  — Streamable HTTP: https://mcp.e2a.dev/mcp + Bearer

The hosted variant is the right choice when deploying to serverless
runtimes (Cloud Run, Lambda, Vercel) where spawning a stdio child
process per request is awkward or impossible — Cloud Run in particular
doesn't host stdio MCP servers at all, so an ADK agent deployed there
literally can't use agent.py.

Verified against the live hosted endpoint:
- MCP initialize handshake: 200, session id assigned
- tools/list: returns all 18 tools, matches the registry
- tools/call list_messages with explicit agent_email: succeeds
- tools/call list_messages without agent_email on a multi-agent
  account: correctly returns "agentEmail is required" (the PR #98
  single-agent prefetch is structurally a no-op when listAgents
  returns >1; a future grant-binding slice would address that)

API shapes verified against actual source, not docs:
- ADK Python: StreamableHTTPConnectionParams(url, headers, timeout)
  (google/adk-python src/google/adk/tools/mcp_tool/mcp_session_manager.py)
- LangChain: transport="streamable_http" + url + headers
  (langchain-ai/langchain-mcp-adapters sessions.py)
- OpenAI Agents: MCPServerStreamableHttp(params={url, headers})
  (openai/openai-agents-python src/agents/mcp/server.py)

Also corrects stale tool counts ("all 11 e2a tools" → drop the count;
we ship 18) across each per-framework README. Top-level
mcp/examples/README.md gets a transport matrix and a stdio-vs-hosted
explainer.

* fix(examples/adk): import McpToolset from deep path; pin google-adk>=2.0 + mcp

google-adk 2.0 stopped re-exporting McpToolset from
google.adk.tools.mcp_tool's package __init__ — only the deep path
google.adk.tools.mcp_tool.mcp_toolset.McpToolset still works.

Bumped requirement to google-adk>=2.0 (was >=1.0) so we don't claim
support for a version where the example doesn't import, and added
mcp>=1.0 as an explicit dep: google-adk 2.0 declared mcp as optional,
but the toolset module imports it unconditionally
(`from mcp import SamplingCapability` at mcp_toolset.py:33), so without
it both agent.py and agent_hosted.py ImportError before they run.

Caught during the build+run smoke for the hosted-variant rollout:
neither agent.py nor agent_hosted.py imported on a fresh
google-adk 2.0 install. The same fix applies to both since they
share the broken import line. With this commit:

  $ python -c "import agent_hosted; print(agent_hosted.root_agent)"
  → imports clean

  $ python -c "...await McpToolset.get_tools(...)..." against the
    hosted endpoint
  → returns all 18 e2a tools

LangChain and OpenAI Agents examples were unaffected — those import
their MCP classes from already-deep paths.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(examples): apply review findings — version floor, timeouts, README

Three findings from the review pass on PR #99:

1. (Low) openai-agents floor was wildly stale: >=0.0.13. The hosted
   variant uses MCPServerStreamableHttp which first shipped in
   v0.0.15 (feat: Streamable HTTP support #643, 2025-05-14). A user
   who pinned the minimum would ImportError before any of our code
   runs — same failure class as the McpToolset/google-adk 2.0
   regression already in this branch. Bumped to >=0.0.15 (verified
   against the v0.0.15 src/agents/mcp/__init__.py snapshot, which
   exports both MCPServerStreamableHttp and MCPServerStreamableHttpParams).

2. (Nit) Inconsistent timeouts across hosted variants:
     ADK             timeout=30
     LangChain       (adapter default, ~undocumented)
     OpenAI Agents   5s default (tight for cold serverless backends)
   Aligned all three at 30s:
     LangChain  → "timeout": timedelta(seconds=30)
     OpenAI     → client_session_timeout_seconds=30
   Both fields verified against the actual library source (already
   in this branch's PR description). ADK already at 30s.

3. (Nit) "wait for a future grant-binding feature" appeared 3x across
   per-example READMEs. Forward-promising language for an unscheduled
   slice. Tightened to just "pass agent_email per tool call" — drops
   the implicit promise without dropping useful information.

Re-smoked all three hosted variants after the changes: each
returns 18 tools against mcp.e2a.dev/mcp.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
jiashuoz added a commit that referenced this pull request May 22, 2026
Prep for publishing to the official MCP Registry (registry.modelcontextprotocol.io).

Two artifacts:

1. mcp/package.json: add `mcpName: "io.github.mnexa-ai/mcp-server"`.
   This is the verification handshake the Registry's publish step
   uses — it confirms the npm package belongs to the publisher
   claiming the registry name. The convention `io.github.OWNER/NAME`
   is required for GitHub-based auth at publish time. Lowercased
   per the Registry's namespacing rules (our GitHub org is
   "Mnexa-AI"; the registry name is "io.github.mnexa-ai/...").

   Also bumps version 0.3.0 → 0.3.1. Two fixes merged since the
   0.3.0 tag — single-agent prefetch (#98) and hosted-MCP example
   variants (#99) — warrant a publish.

2. mcp/server.json: the Registry's canonical server descriptor.
   Lists both transports in one entry:
     - packages[].npm: stdio via @e2a/mcp-server 0.3.1 with the
       three env vars (E2A_API_KEY required + secret, optional
       E2A_AGENT_EMAIL, optional E2A_BASE_URL).
     - remotes[]: streamable-http at https://mcp.e2a.dev/mcp with
       an `Authorization: Bearer {e2a_api_key}` header that
       resolves a registry variable.

   Validated against schema 2025-12-11 — fits maxLength=100 on
   description, all required fields present.

Publish flow (NOT in this PR — requires a maintainer to run):

  1. Merge this PR to main.
  2. Tag mcp-v0.3.1 and push. publish-mcp.yml fires and publishes
     @e2a/mcp-server 0.3.1 to npm with the mcpName field baked in.
  3. brew install mcp-publisher
  4. mcp-publisher login github   (interactive)
  5. cd mcp && mcp-publisher publish

Step 5 reads server.json from the cwd, verifies that the npm
package at @e2a/mcp-server@0.3.1 has the matching mcpName field,
then writes the listing to registry.modelcontextprotocol.io.

After the listing lands, other catalogs (mcp.directory, Glama,
smithery) that cross-reference the Registry will auto-pick us up
on their next sync.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
jiashuoz added a commit that referenced this pull request May 22, 2026
Prep for publishing to the official MCP Registry (registry.modelcontextprotocol.io).

Two artifacts:

1. mcp/package.json: add `mcpName: "io.github.mnexa-ai/mcp-server"`.
   This is the verification handshake the Registry's publish step
   uses — it confirms the npm package belongs to the publisher
   claiming the registry name. The convention `io.github.OWNER/NAME`
   is required for GitHub-based auth at publish time. Lowercased
   per the Registry's namespacing rules (our GitHub org is
   "Mnexa-AI"; the registry name is "io.github.mnexa-ai/...").

   Also bumps version 0.3.0 → 0.3.1. Two fixes merged since the
   0.3.0 tag — single-agent prefetch (#98) and hosted-MCP example
   variants (#99) — warrant a publish.

2. mcp/server.json: the Registry's canonical server descriptor.
   Lists both transports in one entry:
     - packages[].npm: stdio via @e2a/mcp-server 0.3.1 with the
       three env vars (E2A_API_KEY required + secret, optional
       E2A_AGENT_EMAIL, optional E2A_BASE_URL).
     - remotes[]: streamable-http at https://mcp.e2a.dev/mcp with
       an `Authorization: Bearer {e2a_api_key}` header that
       resolves a registry variable.

   Validated against schema 2025-12-11 — fits maxLength=100 on
   description, all required fields present.

Publish flow (NOT in this PR — requires a maintainer to run):

  1. Merge this PR to main.
  2. Tag mcp-v0.3.1 and push. publish-mcp.yml fires and publishes
     @e2a/mcp-server 0.3.1 to npm with the mcpName field baked in.
  3. brew install mcp-publisher
  4. mcp-publisher login github   (interactive)
  5. cd mcp && mcp-publisher publish

Step 5 reads server.json from the cwd, verifies that the npm
package at @e2a/mcp-server@0.3.1 has the matching mcpName field,
then writes the listing to registry.modelcontextprotocol.io.

After the listing lands, other catalogs (mcp.directory, Glama,
smithery) that cross-reference the Registry will auto-pick us up
on their next sync.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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.

1 participant