mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-21 23:06:43 +02:00
Replaces the stub /metrics endpoint with a real prom-client registry
(mana_mcp_ prefix, {service="mana-mcp"} default label). Default
process metrics come along for free.
Policy-gate telemetry is the whole point — without it we can't soak
POLICY_MODE=log-only safely or decide when to flip to enforce. New
counter mana_mcp_policy_decisions_total{decision, reason, mode} buckets
every evaluatePolicy() call:
decision ∈ {allow, deny, flagged}
reason ∈ {admin-scope-not-invokable, destructive-not-allowed,
rate-limit-exceeded, injection-marker, clean, unknown}
mode ∈ {log-only, enforce}
So the rate of "would have been denied" during soak is visible directly
as policy_decisions_total{decision="deny", mode="log-only"}.
Also:
- mana_mcp_tool_invocations_total{tool, outcome} — success |
handler-error | input-invalid. Policy denies are NOT counted here
(they're in policy_decisions_total above); this counter only counts
calls that actually reached the handler or tripped zod validation.
- mana_mcp_tool_duration_seconds histogram per tool/outcome.
Dep: prom-client ^15.1.3 (same version mana-ai pins).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
71 lines
2.3 KiB
TypeScript
71 lines
2.3 KiB
TypeScript
/**
|
|
* mana-mcp — MCP gateway service.
|
|
*
|
|
* Exposes `@mana/tool-registry` over Streamable HTTP, JWT-authed via
|
|
* mana-auth's JWKS. Per-user sessions; admin-scoped tools never reach
|
|
* the wire.
|
|
*
|
|
* Port: 3069. See services/mana-mcp/CLAUDE.md.
|
|
*/
|
|
|
|
import { Hono } from 'hono';
|
|
import { cors } from 'hono/cors';
|
|
import { registerAllModules } from '@mana/tool-registry';
|
|
import { loadConfig } from './config.ts';
|
|
import { authenticateRequest, UnauthorizedError } from './auth.ts';
|
|
import { handleMcpRequest } from './transport.ts';
|
|
import { register as metricsRegistry } from './metrics.ts';
|
|
|
|
// ─── Bootstrap ────────────────────────────────────────────────────
|
|
|
|
const config = loadConfig();
|
|
registerAllModules();
|
|
|
|
const app = new Hono();
|
|
|
|
app.use(
|
|
'*',
|
|
cors({
|
|
origin: config.corsOrigins,
|
|
allowHeaders: ['authorization', 'content-type', 'x-mana-space', 'mcp-session-id'],
|
|
exposeHeaders: ['mcp-session-id'],
|
|
credentials: true,
|
|
})
|
|
);
|
|
|
|
// ─── Health / metrics ─────────────────────────────────────────────
|
|
|
|
app.get('/health', (c) =>
|
|
c.json({
|
|
status: 'ok',
|
|
service: 'mana-mcp',
|
|
registry: { loaded: true },
|
|
})
|
|
);
|
|
|
|
app.get('/metrics', async (c) => {
|
|
const body = await metricsRegistry.metrics();
|
|
return c.text(body, 200, { 'content-type': metricsRegistry.contentType });
|
|
});
|
|
|
|
// ─── MCP endpoint ─────────────────────────────────────────────────
|
|
|
|
app.all('/mcp', async (c) => {
|
|
let user;
|
|
try {
|
|
user = await authenticateRequest(c.req.raw, config.authUrl, config.jwtAudience);
|
|
} catch (err) {
|
|
const msg = err instanceof UnauthorizedError ? err.message : 'Unauthorized';
|
|
return c.json({ error: msg }, 401);
|
|
}
|
|
return handleMcpRequest(c.req.raw, user, config);
|
|
});
|
|
|
|
// ─── Server ───────────────────────────────────────────────────────
|
|
|
|
console.info(`[mana-mcp] listening on :${config.port} (auth=${config.authUrl})`);
|
|
|
|
export default {
|
|
port: config.port,
|
|
fetch: app.fetch,
|
|
};
|