Meridian Commercial Intelligence MCP Server

Meridian Commercial Intelligence MCP Server

Provides governed, deterministic SQL tools over DuckDB for querying sales pipeline data, returning verified results with full source traces. Enables trustworthy natural-language queries about Q1 2026 sales data.

Category
访问服务器

README

Meridian Commercial Intelligence

A trustworthy agentic system that answers natural-language questions about Q1 2026 sales pipeline data. Autonomy — planning, language understanding, dialogue — lives in a Google ADK orchestrator powered by Anthropic Claude. Every number it reports comes from a governed, deterministic MCP tool server over DuckDB, returned with the query and rows that produced it. A thin, executive-grade React client renders the answer with its confidence stamp and full source trace.

Built against REQUIREMENTS-v2.md in this repo — that document is the full spec; this README is the map and the honest account of what shipped.

Table of contents

The trust thesis

A prior version of this tool lost executive confidence because a language model produced numbers, and it produced wrong ones in public. Going agentic does not relax that — it relocates the trust boundary to the tool contract and the orchestrator's system prompt, and it holds two tiers apart:

┌────────────────────────────────────────────────────────────────┐
│  AUTONOMOUS TIER (may be wrong, must be honest)                 │
│  ADK orchestrator on Claude — NL understanding, planning,       │
│  dialogue. Contains an LLM. Bound by system-prompt invariants.  │
└────────────────────────────────────────────────────────────────┘
                       │ MCP tool calls (typed, validated)
                       ▼
┌────────────────────────────────────────────────────────────────┐
│  GOVERNED TIER (cannot be wrong, has NO LLM)                    │
│  governed/ = semantic layer + gates + DuckDB engine.             │
│  Pure deterministic compute. Returns value + stamp + receipt.   │
└────────────────────────────────────────────────────────────────┘

The trusted tier contains no model at all. governed/ never imports agent/, never calls an LLM, and is fully unit-testable on its own — that's tests/test_engine.py and tests/test_gates.py, and neither needs a network connection or an API key.

Ten invariants (REQUIREMENTS-v2.md §1.1) hold this together. The two that matter most for reading this codebase:

  • No model-authored numbers. Every figure in an answer must appear verbatim in a tool result. The agent's system prompt (agent/prompts.py) makes this the first rule, and Tier B evals check the model actually followed it.
  • Abstention is terminal. When data can't support an answer, a tool returns cannot_verify and that's the end of it — not a retry trigger, not a chance for the model to fill the gap.

The Ironbridge case

The canonical regression test, at every layer: "Why did we lose Ironbridge?" OPP-008 is Closed Lost, Enterprise, $195,000 — and loss_reason is empty in the source data. The correct answer is "the record exists, it was lost, and no reason was recorded." Nothing else.

  • Tool layer (governed/engine.py::get_deal_reason): detects the empty field and returns stamp: cannot_verify, never a fabricated cause. Pinned by test_ironbridge_refuses_without_inventing_a_reason (Tier A).
  • Agent layer (agent/prompts.py): the system prompt names this case explicitly and instructs the model to relay the refusal, never to supply a cause of its own. Pinned by test_ironbridge_final_answer_invents_no_cause (Tier B).
  • UI layer (ui/src/components/ConfidenceBadge.tsx): renders the brick cannot_verify badge, and the source trace shows the actual row with its empty loss_reason field.

Why this design

Why ADK + MCP, on Claude, instead of one model doing everything? Because the failure mode this system exists to prevent — a model inventing a number — has to be structurally impossible, not prompt-engineered away. Putting the governed tools behind MCP means the model never sees a code path where it could compute a figure itself; it can only call a tool and relay what comes back. The inner NL→spec translator from the prior version is deleted, not relocated — ADK's native Claude function-calling is the translation layer now, and the translation step never touches the numbers.

Why DuckDB, given the data is 85 rows? At this scale the engine choice is irrelevant to performance, so it's made on the trust axis instead. DuckDB's receipt is an executable, portable SELECT — an analyst can copy the string out of the source trace and run it against the source CSVs to reconcile a figure by hand (INV-7: the SQL in the receipt is the literal string that ran, never a reconstruction). The same SQL dialect scales to a real warehouse, so the prototype and the production data layer are the same choice, not a migration waiting to happen.

How INV-8/INV-9 defend the boundary. A governed tool being correct doesn't help if the model is dishonest about what it returned — that's exactly the shape of the original failure, and it can re-enter through the agent's mouth even with perfect tools underneath. Two mechanisms close that gap:

  • INV-8 (stamp/figure fidelity): the system prompt forbids upgrading assumption or cannot_verify into confident prose, and forbids introducing any number a tool didn't return.
  • INV-9 (receipt propagation): every tool result's receipt travels with the agent's answer, through agent/server.py's /run response, into the UI's source-trace panel. A figure three hops deep is still traceable back to the exact query that produced it.

Architecture

Code boundaries vs. deployment boundaries

The code is factored into three tiers regardless of how it's deployed — that's what makes MCP a real seam rather than decoration:

meridian-agentic/
├── governed/       # GOVERNED TIER — no LLM, no ADK, no UI imports
│   ├── semantic.py     # metric definitions, defined exactly once
│   ├── engine.py       # DuckDB load + the 5 governed executors
│   ├── gates.py        # arg validation, access-scope resolution, invariant post-checks
│   └── models.py       # Pydantic ToolResult / Receipt (the envelope every tool returns)
├── mcp_server/      # wraps governed/ as MCP tools (FastMCP-equivalent MCPServer)
├── agent/           # ADK LlmAgent on Claude — the only LLM in the system
│   ├── agent.py         # tool wrappers (viewer injected via ToolContext, never LLM-visible)
│   ├── prompts.py       # the trust guardrail system prompt
│   └── server.py        # to_a2a() + the UI's /run endpoint, one ASGI app
├── ui/              # thin React (Vite) exec client — no business logic, no LLM key
├── data/            # deals.csv, reps.csv — the only source of truth
└── tests/           # Tier A (hard gate) + Tier B (faithfulness) evals

Demo topology — 2 deployables

  Executives ──►  Thin React UI (nginx-on-Cloud-Run)         [deployable #1]
                       │ HTTPS → agent /run (CORS-locked)
                       ▼
  A2A peers ──►  ADK Agent service (LlmAgent on Claude)      [deployable #2]
                       │ governed tools attached IN-PROCESS as ADK FunctionTools
                       ▼  (in-process call — no network hop)
                 governed/ tier — no LLM
                       ▼
                 DuckDB (in-memory) over data/*.csv

The governed tools run in the agent process for the demo (agent/agent.py registers the governed/engine.py functions directly as ADK FunctionTools — the spec's "simplest" option). mcp_server/server.py still exists as fully separate, independently-runnable code with no import from agent/ — it's just not the wiring the demo agent uses.

Why co-located isn't a corner cut: the governed functions underneath are identical either way. Promoting to the production topology means changing agent/agent.py's tool list from the direct-FunctionTool wrappers to an MCPToolset pointed at a deployed mcp_server/, and nothing in governed/ changes. That's a wiring change, not a rewrite — which is the entire point of having drawn the tier boundary before touching the deploy story.

Production topology — 3 services

  UI (static) ──►  ADK Agent service ──MCP (Streamable HTTP, authed)──►  MCP Server service

mcp_server/server.py deploys as its own Cloud Run service (mcp_server/Dockerfile), reachable over Streamable HTTP, independently callable by any MCP client — not just this agent. See Deployment.

Running it locally

Governed tier + Tier A evals (no API key, no network)

uv sync
uv run pytest tests/test_engine.py tests/test_gates.py tests/test_tool_determinism.py -v

This is the hard gate. If it's not green, nothing downstream is trustworthy enough to run.

The agent, locally

export ANTHROPIC_API_KEY=sk-ant-...
export UI_ORIGIN=http://localhost:5173
uv run uvicorn agent.server:app --reload --port 8080

agent/server.py is one ASGI app exposing:

  • POST /run — {question, viewer, session_id?} → {answer, stamp, receipts[], session_id}, the contract the React UI speaks.
  • GET /.well-known/agent-card.json — the auto-generated A2A agent card (from to_a2a()).
  • GET /livez — liveness check. Named /livez rather than the more common /healthz because on Cloud Run's *.run.app domains, GFE (the edge in front of the container) reserves /healthz for its own internal convention and never forwards it to the app.

Without a key, the agent process will fail to make model calls — there is deliberately no LLM fallback for figures (spec §13.5): if the model is unreachable, the intent is an honest refusal, never a guessed number. To exercise the governed tools without any model, call mcp_server/server.py directly (see below) or hit governed/engine.py's functions from a Python shell — both paths are model-free by construction.

Dev/debug alternative: uv run adk web agent gives ADK's own tool-call-chain visualizer. It is not the executive-facing surface and isn't part of the demo — the React UI is.

The MCP server, standalone

uv run python -m mcp_server.server                 # stdio (demo transport)
MCP_TRANSPORT=streamable-http uv run python -m mcp_server.server   # production transport

Any MCP client can call the five tools directly this way, with no agent in the loop. Every tool requires viewer as an explicit argument here ("LEADERSHIP" or a rep_id like "REP-02") — unlike the agent's LLM-facing schema, this is the self-sufficient governed surface, so it has to take the real argument (see Access control for why the agent's version doesn't).

The UI, in dev

cd ui
npm install
npm run dev   # http://localhost:5173, expects the agent at http://localhost:8080

Set VITE_AGENT_URL (ui/.env or the shell) to point at a different agent instance. The UI never holds the Anthropic key and never calls the MCP server directly (spec §11A) — it only knows the agent's /run endpoint.

The eval suite (what gates a deploy)

Two tiers, mirrored in .github/workflows/deploy.yml:

Tier A — governed-tool determinism (hard deploy gate; no LLM, no network). tests/test_engine.py, tests/test_gates.py, tests/test_tool_determinism.py. Calls the actual MCP tool surface (not just the raw functions) against tests/golden_tool_evals.yaml, and asserts:

Tool call Stamp Figure
get_segment_attainment("Enterprise") assumption 58.6% ($2,050,000 / $3,500,000)
get_segment_attainment("Mid-Market") assumption 56.4% ($1,100,000 / $1,950,000)
get_segment_attainment("SMB") assumption 58.5% ($275,000 / $470,000)
get_reps_at_risk() assumption 7 of 10 below 70%
get_pipeline_value(close_before="2026-03-31") assumption $1,301,000
get_pipeline_value() verified $4,311,000
get_deal_reason("Ironbridge") cannot_verify refuses — loss_reason empty
get_deal_reason("Fulcrum Enterprises") verified "Competitor - Salesforce"

Plus the access-scoping cases: a scoped rep (viewer="REP-02") gets only their own contribution (never the org-level figure, by any phrasing — the filter lives in query construction), an unknown/blank viewer fails closed, and LEADERSHIP is unrestricted. Also asserted: calling any tool twice with identical arguments returns byte-identical results (INV-6), and every receipt's sql field is non-empty.

Gate rule: this suite green, or no deploy. It runs in ~1 second and needs nothing but uv.

Tier B — orchestrator faithfulness (needs a live Claude call). tests/test_agent_faithfulness.py, skipped locally unless ANTHROPIC_API_KEY is set, and run as its own CI job against anthropic/claude-haiku-4-5-20251001 to keep cost down. Asserts the final agent answer — not just the tool result — honors the invariants: no fabricated Ironbridge cause, forecast questions refused as out-of-scope, a segment-attainment answer's figure matches the tool's exactly, a compound comparison calls both tools and self-computes nothing, and access forwarding holds (a scoped rep's final answer never contains the org-level figure). A Tier B failure blocks the agent-service deploy.

Access control

Model: authenticate high, authorize low. Identity is claimed at the UI's login screen (no password — a stubbed claim, spec §11B); authorization is enforced in the governed tier as a mandatory SQL predicate. The LLM is never in the access path.

Mechanically, in agent/agent.py: every tool wrapper takes tool_context: ToolContext instead of viewer: str. ADK detects the ToolContext-typed parameter and (a) excludes it from the JSON schema the model sees — confirmed by inspecting the generated function declarations, none of which contain viewer — and (b) injects the real session object at call time. The wrapper reads tool_context.state["viewer"], populated by agent/server.py from the authenticated login identity on every /run call, and forwards it unchanged into governed/engine.py. The model cannot see it, set it, or be prompt-injected into changing it, because there is no parameter for it to act on.

Enforcement, in governed/gates.py::resolve_viewer + scope_predicate: "LEADERSHIP" gets no filter; a known rep_id gets AND rep_id = ? injected into every query the engine builds, applied to both sides of a ratio (a scoped rep's segment attainment is their own won ÷ their own quota, not their contribution to the segment total); anything else — unknown identity, blank, a segment they don't belong to being asked about — fails closed: an empty cannot_verify result, never full data. The applied scope is written into receipt.assumptions (e.g. "Scoped to your deals (REP-02); org-level figures are restricted to leadership"), so the access boundary is itself part of the auditable receipt.

The honest stub: the login screen's identity is unauthenticated and therefore spoofable — anyone can type REP-02 or LEADERSHIP. That is the deliberate simplification for this demo. Swapping it for a real IdP that sets a verified rep_id claim changes only where viewer originates (an auth middleware in front of agent/server.py's /run endpoint instead of a login form); it does not touch resolve_viewer, scope_predicate, or anything in governed/engine.py — the same promote-without-rewrite pattern as the MCP seam.

A2A and calling the MCP tools directly

agent/server.py builds its ASGI app from google.adk.a2a.utils.agent_to_a2a.to_a2a(root_agent), which auto-generates the agent card at /.well-known/agent-card.json and the A2A JSON-RPC endpoint. Any A2A-aware peer (another ADK agent, Gemini Enterprise, a different framework entirely) can discover the card and delegate pipeline questions without going through the React UI at all.

To call the governed tools with no agent in the loop, run mcp_server/server.py (stdio or Streamable HTTP per above) and connect any MCP client to it — list_supported_questions is a reasonable first call to confirm the connection and see the capability set.

Personalizing the UI

Everything client-specific lives in ui/config.ts: BRANDING (client name, title line, context line, tagline), the stubbed REP_ROSTER login identities, and SUGGESTED_QUESTIONS (the chips, including the dashed Ironbridge trap). VITE_AGENT_URL points the build at a specific agent deployment. Re-skinning for a new engagement is editing this one file and rebuilding — no component code changes.

Deployment

CI (.github/workflows/deploy.yml) on push to main: ruff → Tier A → (in parallel) build the UI bundle and run Tier B → build + push both container images → deploy. No deploy if Tier A fails; a Tier B failure blocks the agent-service deploy specifically. Auth to GCP is keyless (Workload Identity Federation) — no JSON key ever lives in this public repo.

  • Agent service: agent/Dockerfile, non-root, uv-managed, runs uvicorn agent.server:app. ANTHROPIC_API_KEY is injected from Secret Manager at deploy time — it is never baked into the image and never present anywhere but this one service.
  • UI: ui/Dockerfile, multi-stage (npm run build → static nginx), VITE_AGENT_URL passed as a build arg pointing at the deployed agent URL.
  • MCP server (production only): mcp_server/Dockerfile, not deployed in the demo topology. Splitting it out is documented above under Architecture and is a config change to agent/agent.py's tool wiring, not a rewrite.

Enterprise alternative: deploy the agent to Vertex AI Agent Engine instead of raw Cloud Run for managed scaling/registry.

Scope cuts

Deliberate, not oversights (spec §5.2):

  • Read-only. No write-back to any system of record.
  • No auth/user management beyond Cloud Run IAM, MCP-server auth (production only), and the UI↔agent CORS lock — the login screen's identity is a stub, documented as one above.
  • No forecasting, and no "why" beyond a stored loss_reason — the Ironbridge case is the canonical example of exactly the boundary this draws.
  • Data ships as CSVs; no live warehouse connector.
  • Exactly five governed metrics. Anything else is a navigational refusal via list_supported_questions, not an approximation.
  • The UI is intentionally thin: ask, answer, verify. No dashboards, saved views, or exports.

What this proves, and what it doesn't

Proves: that an LLM can sit in the planning/dialogue loop of a numbers-reporting tool without ever being the source of a number — the governed tier is fully independent, unit-tested without any model, and every figure the agent relays is traceable through a receipt back to an executable SQL statement. That access control can be enforced without putting the model anywhere near the enforcement path. That the trust properties survive an extra hop (agent → A2A → external caller, or agent → UI) because the receipt is carried as data, not re-derived at each hop.

Doesn't prove: that Claude's tool-selection is perfect on arbitrary phrasing — Tier B checks specific canonical and adversarial-ish questions, not exhaustive phrasing coverage. That the login screen is a real authentication system — it isn't, by design, and the README says so rather than letting the UI imply otherwise. That this scales past a demo dataset — DuckDB and an in-memory 85-row load are the right choice for this exercise specifically, with an explicit note about what changes (warehouse connection string, nothing else in the SQL) if it didn't.

Versions

Pin exact versions at build time; re-verify against current docs before a real deployment. Installed in this repo's uv.lock at the time of writing: google-adk==2.5.0, mcp==2.0.0 (its FastMCP is now mcp.server.MCPServer — same tool-decorator shape, see mcp_server/server.py's module docstring), litellm==1.94.0, a2a-sdk per pyproject.toml, Python 3.12, React 18 + Vite 5. Default model: anthropic/claude-haiku-4-5-20251001 (MODEL_ID env var; set to anthropic/claude-sonnet-5 for an exec demo). References:

  • ADK ↔ MCP integration: https://google.github.io/adk-docs/tools/mcp-tools/
  • ADK models (LiteLLM / Claude): https://google.github.io/adk-docs/
  • Exposing an ADK agent via A2A: https://google.github.io/adk-docs/a2a/quickstart-exposing/

推荐服务器

Baidu Map

Baidu Map

百度地图核心API现已全面兼容MCP协议,是国内首家兼容MCP协议的地图服务商。

官方
精选
JavaScript
Playwright MCP Server

Playwright MCP Server

一个模型上下文协议服务器,它使大型语言模型能够通过结构化的可访问性快照与网页进行交互,而无需视觉模型或屏幕截图。

官方
精选
TypeScript
Magic Component Platform (MCP)

Magic Component Platform (MCP)

一个由人工智能驱动的工具,可以从自然语言描述生成现代化的用户界面组件,并与流行的集成开发环境(IDE)集成,从而简化用户界面开发流程。

官方
精选
本地
TypeScript
Audiense Insights MCP Server

Audiense Insights MCP Server

通过模型上下文协议启用与 Audiense Insights 账户的交互,从而促进营销洞察和受众数据的提取和分析,包括人口统计信息、行为和影响者互动。

官方
精选
本地
TypeScript
VeyraX

VeyraX

一个单一的 MCP 工具,连接你所有喜爱的工具:Gmail、日历以及其他 40 多个工具。

官方
精选
本地
graphlit-mcp-server

graphlit-mcp-server

模型上下文协议 (MCP) 服务器实现了 MCP 客户端与 Graphlit 服务之间的集成。 除了网络爬取之外,还可以将任何内容(从 Slack 到 Gmail 再到播客订阅源)导入到 Graphlit 项目中,然后从 MCP 客户端检索相关内容。

官方
精选
TypeScript
Kagi MCP Server

Kagi MCP Server

一个 MCP 服务器,集成了 Kagi 搜索功能和 Claude AI,使 Claude 能够在回答需要最新信息的问题时执行实时网络搜索。

官方
精选
Python
e2b-mcp-server

e2b-mcp-server

使用 MCP 通过 e2b 运行代码。

官方
精选
Neon MCP Server

Neon MCP Server

用于与 Neon 管理 API 和数据库交互的 MCP 服务器

官方
精选
Exa MCP Server

Exa MCP Server

模型上下文协议(MCP)服务器允许像 Claude 这样的 AI 助手使用 Exa AI 搜索 API 进行网络搜索。这种设置允许 AI 模型以安全和受控的方式获取实时的网络信息。

官方
精选