MCP + Claude Code
Turning a Roster Database Into an MCP Server Your SMEs Can Query
Result:
One MCP server exposing five tools (account roster, ICP fit, personas, SEO/AEO signals, and a combined roster-intelligence query) replaces three separate lookups, a CRM query, a spreadsheet, and a Slack ping to whoever owns the dashboard, with one Claude conversation. The ICP baseline itself is editable by a non-engineer through a real admin page in this repo, not a migration someone has to run for them.
Problem#
An SME (sales, GTM, content) who wants to know which accounts to work this week needs three different answers from three different places: an ICP fit score from a Postgres view, which specific person at that account to reach from a CRM or spreadsheet, and whether that account has been engaging with the site at all from a PostHog dashboard. None of those systems talk to each other, and none of them are things a non-engineer can query directly, so the real workflow is a Slack message to whoever owns each piece. An MCP server collapses that into one conversation: a client like Claude calls a small set of tools and gets back a joined answer, without the SME needing to write SQL, open three tabs, or wait on someone else’s afternoon.
Pattern#
1. Extend the roster schema, don’t rebuild it#
The account-level ICP fit score already has a home: a dynamic ICP model
scores every account 0–1 against an authored icp table (segment, size band, industry,
region) and blends that score with a weighted moving average of live conversion and revenue data.
Roster intelligence doesn’t re-derive that scoring, it joins two more tables alongside it:
persons: one row per individual contact at a fit account, scored the same feedback-loop way but at the individual level. See the persona model for the pattern.signals: SEO/AEO visibility and website engagement per account, so whether an account is engaging with anything published becomes a joinable column instead of a separate dashboard. See feeding SEO/AEO into the roster.
An account row, its ICP fit, its top personas, and its signal data all share the same account id, so joining them is one query, not three systems.
2. Expose the roster through MCP tools, not a REST API#
A REST API still needs a client to know its shape in advance. MCP tools are self-describing: a client like Claude reads the tool definitions and figures out which one answers the question in front of it. Five tools cover the roster:
get_account_roster: list accounts matching a filter (segment, region, minimum ICP score).score_icp_fit: delegates straight to the existing weighted model above.list_personas: the ranked buyer personas for one account.get_seo_signals: that account’s SEO/AEO visibility and site engagement.get_roster_intelligence: the combined answer, fit, top persona, and signal strength for one account or a filtered list, in one call.
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
server.tool(
"get_roster_intelligence",
{
minIcpScore: z.number().min(0).max(1).default(0.5),
region: z.string().optional(),
},
async ({ minIcpScore, region }) => {
const accounts = await db.query(
`select a.id, a.name, f.icp_score, s.signal_strength
from accounts a
join icp_fit_score f on f.account_id = a.id
join account_signals s on s.account_id = a.id
where f.icp_score >= $1 and ($2::text is null or a.region = $2)
order by f.icp_score desc, s.signal_strength desc
limit 25`,
[minIcpScore, region ?? null],
);
return { content: [{ type: "text", text: JSON.stringify(accounts) }] };
},
);Scaffold with npm install @modelcontextprotocol/sdk zod, test each tool with npx @modelcontextprotocol/inspector node index.ts before wiring the server into anything, then
register it the same way as any other MCP server: .mcp.json for Claude Code,
claude_desktop_config.json’s mcpServers entry for Claude Desktop.
3. Make the ICP baseline itself editable by an SME#
Everything above assumes the icp table already has rows in it, and that’s the part a
non-engineer previously couldn’t touch without asking someone to run a migration. Two ways to fix
that:
A real admin page. This repo now ships one: /admin/icp-baselines, password-gated the same
way as the existing /admin/recipe-requests (HTTP Basic Auth against an ADMIN_PASSWORD secret,
fails closed if it’s unset), backed by a Cloudflare Workers KV namespace instead of a live
Postgres connection. It lists the current baseline rows and lets an SME add or update one
(segment, employee range, industry, region) through a plain form, no SQL required.

A git-tracked alternative. Teams who’d rather version ICP criteria in git than edit it live can seed the same shape from a committed file instead:
# icp-baselines.yaml
- segment: "Mid-market SaaS"
minEmployees: 200
maxEmployees: 2000
industry: "SaaS"
region: "EMEA"A small script reads the file and upserts each row into the same store the admin page writes to,
so a pull request against icp-baselines.yaml is reviewable the way a raw migration against a
live table never is.
4. Tie it together with the prompts an SME types#
Once the tools are registered, the whole point is that nobody needs to know the schema underneath. Example prompts a reader can hand straight to Claude once this server is connected:
- “Which accounts in the EMEA region are scoring above 0.7 on ICP fit and have real site engagement this month?”
- “Who’s the best person to reach at [account], and have they looked at anything we’ve published?”
- “Add a new ICP segment for enterprise retail, 2000 to 50000 employees, and show me who already matches it.”
That last one is the whole pattern in one sentence: an SME editing the model and querying it in the same conversation, with no engineer in the loop for either step.
Output#
Build this and the roster question moves from three systems to one conversation: an SME asks Claude for accounts above a fit threshold with real engagement, gets a joined answer back, and edits the ICP baseline that produced it, all without opening a database client or waiting on someone else’s afternoon. The pattern is proven here, not just described. The screenshot above is a real page in this repo, saving real rows through a real Cloudflare Workers KV binding, so every step in the Pattern section is something a reader can rebuild and click through, not a diagram of an idea that was never run.
Variations#
- Swap KV for D1 or Postgres at scale. A single JSON blob in KV is enough for a few hundred ICP rows; a team managing thousands of segment/persona combinations should move the same shape into a real relational store and keep the MCP tool layer identical.
- Add write-tools once the read-only pattern is trusted. Every tool above is read-only by design; letting an SME edit personas or signals directly from Claude, not just query them, is the natural next step once the roster’s accuracy is proven out.
- Audit-log SME edits. The admin page above accepts any authenticated edit; a team relying on this for real targeting decisions should log who changed which baseline and when, the same discipline any CRM field change would get.