@phoenixvc/mcp-http
v0.1.0
Published
In-process product MCP HTTP envelope: fail-closed Bearer, RFC 9728 challenge, health probe. Extracted from Baton and Docket.
Maintainers
Readme
@phoenixvc/mcp-http
In-process product MCP HTTP envelope shared by Baton and Docket.
This is the extractable pattern. It is not the @phoenixvc/mcp stdio OAuth CLI.
What this package is
The envelope every product API copies:
- MCP lives on the same process and port as the product HTTP API.
- Mount at
/mcp. Leave/healthpublic. - Guard
/mcpwithAuthorization: Bearer ${MCP_SECRET}(machine / orchestrator lane). - Fail closed (503) if the secret is missing outside local/dev.
- Optional RFC 9728
WWW-Authenticate+ protected-resource metadata when a human OAuth lane exists.
Tool handlers stay in the product. Mystira introspection, project grants, and actor API keys stay in Baton.
Node
import { applyMcpSecretAuth, mcpHealthPayload } from "@phoenixvc/mcp-http";
app.post("/mcp", async (req, res) => {
if (!applyMcpSecretAuth(req, res, { secret: process.env.MCP_SECRET })) return;
// existing Streamable HTTP / FastMCP handler
});
app.get("/health/mcp", (_req, res) => {
res.json(mcpHealthPayload({
name: "my-product-mcp",
tools: ["list_things"],
authRequired: Boolean(process.env.MCP_SECRET),
}));
});OAuth-ready products (Baton) add:
import {
protectedResourceMetadataUrl,
sendProtectedResourceMetadata,
} from "@phoenixvc/mcp-http";
const resource = "https://mcp.example.com/mcp";
app.get("/.well-known/oauth-protected-resource/mcp", (_req, res) => {
sendProtectedResourceMetadata(res, {
resource,
authorization_servers: ["https://identity.example/"],
scopes_supported: ["example.read"],
});
});Pass resourceMetadataUrl: protectedResourceMetadataUrl(resource) into applyMcpSecretAuth so 401s carry resource_metadata.
Python (Docket recipe — not shipped as a wheel)
Use FastMCP's http_app(stateless_http=True) mounted at /mcp. Guard with a pure ASGI middleware (not Starlette BaseHTTPMiddleware — it buffers and breaks streamable HTTP):
- Secret set →
hmac.compare_digest(auth, f"Bearer {secret}"), else 401 +WWW-Authenticate: Bearer. - Secret unset and
ENVIRONMENTnot in{development, local, test}→ 503. - Mirror
authorizeMcpSecret({ missingSecretMode: "local-open" }).
What not to extract
- Product tools (tasks, cost KQL, YAML mirrors).
- Baton actor identity / project membership.
- A stdio OAuth runner — use native HTTP MCP clients, or
mcp-remote, or the official SDKStreamableHTTPClientTransport.
