Clearinghouse

Clearinghouse

A single-auth MCP server that gives Claude unified access to deal intelligence from Salesforce, Slack, and Gong, enabling natural-language queries about deal status, activity, and pipeline health.

Category
访问服务器

README

Clearinghouse

Single-auth GTM deal-intelligence MCP server for 1upHealth. One connector in Claude (https://mcp.1uphealth.com/mcp), one "Continue with Google" sign-in, and every deal answer the company already has in Gong, Salesforce, and Slack — without anyone ever connecting those three systems themselves. The user's token stops at this server; downstream systems are reached with org credentials that never leave GCP.

PRD (v1.0 draft, 2026-08-05): the Claude artifact "Clearinghouse — PRD".

Status — Weeks 1–4 slice

  • OAuth 2.1 resource server — RFC 9728 metadata at /.well-known/oauth-protected-resource, the 401 + WWW-Authenticate: resource_metadata handshake Claude requires, JWT verification against the rented authorization server via JWKS.
  • find_deal — fuzzy deal name → real Salesforce opportunities.
  • deal_status — the Salesforce picture of one deal. Its coverage field points at the two tools below rather than returning a silently thin answer.
  • deal_channel_activity — a deal's synced Slack activity. Not a separate Slack API: messages arrive in Salesforce as slackv2__Slack_Message__c records joined to the Opportunity, so there's no channel-Id concept and no separate credential. External senders are flagged, not hidden — see the live-setup note below on how that's inferred.
  • call_details — recent Gong calls on a deal: when, how long, who was on them, and Gong's brief only once Decision D is answered (see below).
  • recent_activity — no deal Id required: the most recently modified Salesforce opportunities in a window, each annotated with how many Slack messages and Gong calls landed on it in the same window. For "what should I catch up on."
  • coverage_check — bulk data-hygiene sweep across open deals: which ones have no Slack activity synced, no next step, or no Gong call on file. deal_status's own coverage field answers this one deal at a time; this is the many-deals-at-once version.
  • pipeline_snapshot — the read-only, Claude-facing sibling of pipeline-pulse: same fiction detection (ghost_expansion, renewal_blindspot, stale_momentum), answered inline instead of on a schedule, and it never proposes or writes anything to Planhat.
  • Roster gate — Git-backed roster.json, deny by default, denial audited.
  • Audit — every tool call logged as one JSON line (actor, tool, args, systems, bytes, latency) for the Cloud Logging → BigQuery sink.
  • Injection guards — typed enumerated inputs, one escape path for SOQL, Salesforce Ids validated by shape, LIMIT capped server-side; free-text fields (deal descriptions, Slack messages, Gong call titles and briefs) returned inside a labeled external-data envelope.
  • pipeline-pulse routine — a service-to-service job (not a Claude tool) that cross-references Salesforce opportunities against Planhat companies to flag "fictions": pipeline data that looks fine but isn't (ghost_expansion, renewal_blindspot, stale_momentum). See below.
  • /dashboard — a browser UI behind the same roster gate as /mcp, for people who want the data directly instead of asking Claude: deal lookup, a pipeline-pulse review board, and roster/audit admin. See below.

Not yet built (Week 5+): the Gong nightly index that replaces the window scan, per-person budgets, directory sync.

Run locally

npm install
cp .env.example .env       # defaults: AUTH_MODE=dev, SF_MODE=mock, PLANHAT_MODE=mock
npm run dev
npm test

Smoke-test with MCP Inspector: npx @modelcontextprotocol/inspector, connect to http://localhost:8080/mcp (streamable HTTP). In dev mode identity comes from DEV_USER_EMAIL, which must be on roster.json. Dev mode refuses to start when NODE_ENV=production.

Launch gates → where they live

Gate What Where
01 Fake-"Claude" phishing DCR off, one pinned client, one redirect URI (https://claude.ai/api/mcp/auth_callback) Auth-vendor dashboard — two settings, do this the day the tenant exists
02 Lookalike connector URL Host on 1uphealth.com itself; connector-adding restricted to Claude org admins Decision A + Claude admin console
03 Gong tenant walk call_details reaches calls only through a resolved Salesforce opportunity; GongClient exposes no list-all and no by-call-Id path, and the live window scan is page-capped src/gong/types.ts, src/gong/live.ts
04 PHI on sales calls GONG_CONTENT=metadata means the call brief is never requested — no redaction pass is load-bearing. Turning it on needs GONG_PHI_REVIEW_SIGNED_OFF=true or the server refuses to start Decision D — src/config.ts, still unanswered
05 Salesforce signing key JWT bearer as one pre-authorized integration user (src/salesforce/client.ts, shared by LiveSalesforce and LiveSlack); key generated in Cloud Shell, never a laptop; connected app locked to that user + static egress IP Code + Salesforce setup runbook below
06 Roster misconfiguration Git-backed roster.json, service reads only, deny by default, denials audited src/roster.ts, src/http/auth.ts

Salesforce live setup (Gate 05 runbook)

  1. Create integration user clearinghouse@1uphealth.com with a read-only profile scoped to Opportunity + Account, plus read on slackv2__Slack_Message__c if SLACK_MODE=live — Slack activity is synced in by a Slack managed package, not a separate bot API, so this one connection covers both. (isExternal on a message is a heuristic — whether it resolved to a Contact/Lead, not a real field. Verify against real messages before relying on it.)
  2. In Cloud Shell (so the key never touches a laptop): openssl req -x509 -newkey rsa:2048 -nodes -keyout sf.key -out sf.crt -days 730
  3. Connected app: upload sf.crt, enable OAuth, scopes api; Admin approved users are pre-authorized; assign only the integration user's profile; set Relax IP restrictions = Enforce, allowlist the Cloud Run static egress IP (via Serverless VPC connector + Cloud NAT).
  4. Put sf.key in Secret Manager as SF_PRIVATE_KEY; delete the local copy.
  5. Set SF_MODE=live, SF_CLIENT_ID (consumer key), SF_USERNAME.

Gong live setup (Gates 03 + 04 runbook)

  1. Gong admin → API key for this service only; store both halves in Secret Manager as GONG_ACCESS_KEY / GONG_ACCESS_KEY_SECRET.
  2. Leave GONG_CONTENT=metadata. In this mode the content selector sent to Gong omits content.brief entirely, so no spoken content reaches this process, its memory, or its logs — the gate is the request, not a filter.
  3. Before flipping to GONG_CONTENT=summaries: confirm the BAA and retention terms covering call recordings (Decision D), then set GONG_PHI_REVIEW_SIGNED_OFF=true. Live Gong refuses to boot with summaries on while that is false — deliberately two hands, not one flag.
  4. Verify the response shape first. src/gong/live.ts follows Gong's documented /v2/calls/extensive payload (metaData, parties[].affiliation, context[].objects[]) but has never run against 1upHealth's tenant. Check one real call before GONG_MODE=live.
  5. GONG_LOOKBACK_DAYS (default 180) bounds the window scan. This is the direct-read stand-in for the PRD's nightly index; when call volume makes the scan slow, the index lands behind the same GongClient interface.

Pipeline-pulse routine (Planhat)

POST /routines/pipeline-pulse is a separate, service-to-service path — not part of the /mcp connector Claude talks to, and not reachable by an end-user Claude session. It's meant to be triggered by Cloud Scheduler, not a person.

What it does: pulls Salesforce opportunities and Planhat companies, then runs three deterministic detectors over them —

  • ghost_expansion — a Planhat expansion signal with no real health behind it (see src/fictions/rules/ghostExpansion.ts)
  • renewal_blindspot — a Planhat renewal coming due (or overdue) with no open Salesforce opportunity anywhere near that date
  • stale_momentum — a late-stage opportunity that's gone quiet in both Salesforce and Planhat

High-confidence findings can propose a [DRAFT] Planhat project as a follow-up; everything else surfaces as a Slack nudge (suggestedAction). ROUTINES_DRY_RUN=true by default means the routine always computes and returns what it would create in Planhat without ever calling the write method — the preview response and the real write share the same code path (toProjectDraft in src/routines/pipelinePulse.ts), so the preview can't drift from reality.

Before setting PLANHAT_MODE=live or ROUTINES_DRY_RUN=false:

  1. src/planhat/live.ts's field mapping (RawCompany) is a best-effort guess at Planhat's schema — a real-tenant lookup was started but never finished. Verify it against one real company record first.
  2. Generate a Planhat API token scoped to this service, store it as PLANHAT_API_TOKEN (Secret Manager in Cloud Run, never a file in the repo).
  3. Generate a random secret ≥16 chars for ROUTINES_SHARED_SECRET; whoever calls the route (Cloud Scheduler) sends it as x-routines-secret. Unset = the route always 403s, independent of whether the rest of the server is configured.
  4. Only after (1) is confirmed against real data, flip ROUTINES_DRY_RUN=false — until then, run it in dry-run and read the proposedProjects it returns.

Local dashboard

/dashboard is a second, human-facing surface — same server, same bearerAuth + roster gate as /mcp, just a browser tab instead of a Claude conversation. For anyone who wants to look something up directly, or for demoing/debugging without going through Claude at all. Vanilla JS, no build step, no separate deploy.

Six tabs:

  • Overview — the default landing tab: a RevOps one-page rollup instead of five separate places to look. Open pipeline value and deal count, open pipeline by stage, fictions by severity (same detection as pipeline-pulse), a "needs attention" list of the top fictions, coverage-check's gap count, and upcoming Planhat renewals sorted soonest-first (overdue ones sort to the top, not the bottom — a lapsed renewal date is still the most urgent thing on the list). Pure rollup — src/routines/overview.ts — no new system calls beyond what the other tabs already make, and no write path.
  • Deal lookup — search by name (find_deal), pick a result, see the same combined Salesforce + Gong + Slack picture deal_status / deal_channel_activity / call_details give Claude.
  • Recent activity — recent_activity as a form: an owner filter and a day count instead of a deal Id.
  • Coverage check — coverage_check as a form: which open deals have no Slack activity synced, no next step, or no Gong call.
  • Pipeline-pulse — a "Run pipeline-pulse (dry run)" button that lists the fictions it finds and the Planhat projects it would propose. This button always forces dryRun: true, regardless of ROUTINES_DRY_RUN in config — a person clicking a button in a browser should never be the thing that writes to Planhat; only the Cloud Scheduler-triggered /routines/pipeline-pulse path can do that, and only once (1) in that section's runbook is done. (No separate tab for pipeline_snapshot — same underlying scan, so this tab already covers it; pipeline_snapshot exists as its own MCP tool because Claude needs a read-only-only version with no proposed-projects section at all.)
  • Admin & audit — the roster's members, and a live tail of the most recent audit events (in-memory, capped, lost on restart — the durable trail is still Cloud Logging → BigQuery; this is a convenience view, not a second source of truth).

Free text from external systems (deal descriptions, Slack messages, Gong call titles) is written into the page with textContent, never innerHTML — same untrusted-data handling as everywhere else in this codebase, just for a human reader instead of an LLM.

Auth vendor setup (Decision C — vendor-agnostic checklist)

Whichever of WorkOS AuthKit / Auth0 / Stytch / Descope is chosen, on auth.1uphealth.com (CNAME so the vendor stays swappable):

  1. Federate to Google Workspace; restrict to the 1uphealth.com hosted domain.
  2. Disable dynamic client registration (Gate 01).
  3. Register exactly one client, redirect URI https://claude.ai/api/mcp/auth_callback, PKCE required.
  4. Access tokens: JWT, audience = https://mcp.1uphealth.com/mcp, include email. Long-lived rotating refresh tokens with reuse detection — Claude's background refresh is unreliable, and a dead refresh token must return exactly invalid_grant or users get wedged.
  5. Do not put a WAF in front of the IdP that blocks Anthropic egress (160.79.104.0/21 must reach both this server and the AS).
  6. Fill AUTH_ISSUER + AUTH_JWKS_URL, set AUTH_MODE=oauth.

Deploy (Cloud Run)

  • Stateless by construction: one MCP server instance per request, JSON responses, no sessions — safe under autoscaling.
  • Public ingress; OAuth enforced in-app (Claude cannot mint Google IAM tokens, so Cloud Run IAM protection is unusable).
  • roster.json ships in the image; the deploy pipeline is the only writer (Gate 06). CI syncs it from the repo — or from the restricted Google group if Decision E lands that way.

Open decisions (PRD §08)

Decision Recommendation Where it lands here
A Domain mcp.1uphealth.com + auth.1uphealth.com; register lookalikes PUBLIC_URL, AUTH_ISSUER
B One access tier or two One tier for v1, stated at consent roster stays one flat list
C Auth vendor Any of the four; keep DNS ours AUTH_ISSUER/AUTH_JWKS_URL only
D PHI / BAA gate Confirm Claude-plan retention terms + GCP BAA; only decision that can change the product GONG_CONTENT + GONG_PHI_REVIEW_SIGNED_OFF. Still open — Gong ships metadata-only until it is answered
E Roster ownership One restricted Google group, synced by CI replaces hand-edits to roster.json

Deliberately not building

No workspace-wide Slack search, no stored Slack messages, no write tools in the Claude-facing connector, ever, no admin tools inside the connector (PRD §06). The one write path in this repo — pipeline-pulse creating draft Planhat projects — is a separate service-to-service routine Claude can never reach, dry-run by default, and still gated on an unverified field mapping (see above).

推荐服务器

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 模型以安全和受控的方式获取实时的网络信息。

官方
精选