MSO Cloud · Documentation

MCP server — plug UCIP data into Claude / Codex / Cursor

Source: docs/product/mcp-server.md Updated 2026-09-21
On this page

Trạng thái: GA. Super-admin gated. Surface tool bám theo CHAT_TOOL_DEFS (nguồn chuẩn: apps/web/src/server/chat/tools.ts) — không bao giờ chứa market tool (§58).

UCIP exposes its chat tools (the always-on CHAT_TOOL_DEFS surface — queryMetric, topNByDimension, compareMetricAcrossPeriods, explainMetricDelta, growthPlaybook, msoOperatingLoop, forecastMetric, livestreamSessions, getOrderDetail, first-party marketing tools, listMetrics, getContext) qua Model Context Protocol — agentic standard maintained dưới Linux Foundation by Anthropic + OpenAI + Google + Microsoft + AWS. Bất kỳ MCP-compatible client nào trên máy operator đều query được dữ liệu UCIP của tổ chức mình bằng natural language: Claude Desktop, Claude Code CLI, OpenAI Codex CLI, Cursor, Goose, ChatGPT Apps, …


Mental model#

┌─────────────────────┐      stdio        ┌────────────────────┐    HTTPS    ┌────────────┐
│  Claude Desktop     │ ◄───────────────► │  yng-mcp-server    │ ◄────────► │  UCIP web  │
│  Claude Code CLI    │   (MCP wire)      │  (apps/mcp-server) │  Bearer    │  /api/mcp  │
│  Codex CLI          │                   │   stdio bridge      │   token    │            │
│  Cursor / Goose / …  │                   └────────────────────┘             └────────────┘
└─────────────────────┘
  • Client (left) talks MCP over stdio with the bridge.
  • Bridge (apps/mcp-server) translates MCP tools/list + tools/call requests into HTTPS calls against UCIP.
  • UCIP validates the bearer token, pins RLS, runs the tool, returns JSON.

The bridge is intentionally thin — adding a new chat tool to CHAT_TOOL_DEFS reflects automatically the next time the client restarts.


Quick start#

1. Super admin: mint a token

  1. Log in with a super-admin account (seeded accounts: scripts/CREDENTIALS.md) → /admin/orgs → click the org you want to expose.
  2. In the drawer, switch to the "MCP tokens" tab.
  3. Fill in:
  • Label — who is this for? e.g. ops@younetgroup.com — Claude Desktop.
  • TTL (days) — default 90.
  1. Click Mint token. Copy the green-banner token NOW (yngmcp_<base64url>) — it's shown once, hashed at rest after.
  2. Hand the token to the operator via 1Password / Vault / encrypted note. Never paste it into Slack / email plain-text.

2. Operator: configure the client

The same yng-mcp-server binary works for every MCP client. Pick yours below.

Claude Desktop (macOS)

Edit ~/Library/Application Support/Claude/claude_desktop_config.json:

{
  "mcpServers": {
    "yng-ucip": {
      "command": "pnpm",
      "args": ["--silent", "tsx", "/Users/<you>/.../yng-mso-platform/apps/mcp-server/src/index.ts"],
      "env": {
        "UCIP_API_URL": "http://localhost:3000",
        "UCIP_API_TOKEN": "yngmcp_paste_token_here"
      }
    }
  }
}

Restart Claude Desktop. Open the conversation, you should see a 🛠 yng-ucip indicator at the bottom with the yng-ucip tools listed.

Claude Code CLI

One command — Claude Code persists the config in ~/.claude/.mcp.json:

claude mcp add yng-ucip \
  -e UCIP_API_URL=http://localhost:3000 \
  -e UCIP_API_TOKEN=yngmcp_paste_token_here \
  -- pnpm --silent tsx \
  /Users/<you>/.../yng-mso-platform/apps/mcp-server/src/index.ts

claude mcp list   # verify

In a new claude session, the model auto-discovers the tools.

Codex CLI

codex mcp add yng-ucip \
  -e UCIP_API_URL=http://localhost:3000 \
  -e UCIP_API_TOKEN=yngmcp_paste_token_here \
  -- pnpm --silent tsx \
  /Users/<you>/.../yng-mso-platform/apps/mcp-server/src/index.ts

codex mcp list    # verify

Cursor / Goose / others

Same recipe — point the client at the apps/mcp-server/src/index.ts entry with UCIP_API_URL + UCIP_API_TOKEN in env. The bridge is stdio-only today.

3. Verify

In your client:

"Liệt kê các chỉ số UCIP có thể truy vấn."

The model should call listMetrics and return the metric catalog (gmv, nmv, aov, …). Then try:

"GMV tháng này so với tháng trước thế nào?"

The model calls queryMetric({ metricSlug: 'gmv', dateRange: { preset: 'this_month' }, compare: true }) and returns summary + time series.


Available tools (catalog mirrors the chat surface)#

Tool Intent Returns
listMetrics Discover what metrics exist { metrics: [{ slug, label, unit, dimensions, … }] } (full catalog)
getContext Anchor "now" + active org { orgId, today, timezone: 'Asia/Ho_Chi_Minh' }
queryMetric One metric, period, optional compare { metric, summary, timeSeries, previousTimeSeries, chart }
topNByDimension Ranking by platform / creator / sku { items: [{ label, value }], chart: { kind: 'bar' } }
compareMetricAcrossPeriods A vs B (explicit, e.g. Q1 vs Q2) { periodA, periodB, delta, deltaPercent, chart }
explainMetricDelta Why did metric change? { contributors: [{ label, current, previous, delta }] } top 10
forecastMetric N days ahead (uses pred_model_registry) { baseline, forecast, model: { slug, version, method } }

Every tool runs through the same RLS + ICT-bucketing + NMV-formula guarantees as the in-app chat surface. The MCP path bypasses the chat conversation overhead (no chat_messages row, no chat_tool_calls row) — instead each call writes ONE row to audit_logs with action='mcp.tool.call'.


Security model#

  1. Token issuance is super-admin only. admin.mintMcpToken is gated by superAdminProcedure. Org admins cannot self-mint. Audit trail in audit_logs (admin.mcp_token.mint).
  2. Tokens are hashed at rest (sha256). Cleartext is shown ONCE in the mint dialog — never retrievable after.
  3. Tokens are per-org, not per-user. Treat them as service accounts. Rotate every 90 days (default TTL). Revoke immediately when the owner leaves or a laptop is lost.
  4. Rate-limited. 30 req/min per IP at the bridge, mirroring the chat surface. 60 req/min on the catalog endpoint (rarely-changing).
  5. RLS preserved. Every tool call pins setOrgContext(token.orgId) before hitting Postgres. A token for org A can never see org B's data.
  6. No write tools. Today the catalog is read-only (querying metrics). Adding a write tool would require a separate review.
  7. Audit log per call. audit_logs row carries: actorUserId = super admin who issued the token, resourceId = tokenId, diffAfter = { tool, durationMs, ok }. Super admin can query the org's audit history under /admin/orgs/[orgId] → Audit tab.
  8. Revocation is instant. Hitting Revoke in the admin drawer sets isRevoked=true — next call returns HTTP 410. The bridge surfaces the error in stderr so the operator sees why.

Troubleshooting#

Symptom Likely cause Fix
[yng-mcp-server] FATAL: UCIP_API_TOKEN env var is required Env var not set or empty Re-export UCIP_API_TOKEN in the client's MCP env block
UCIP auth failed (401): invalid_token Token revoked or expired Super admin mints a new token (drawer → MCP tab)
UCIP auth failed (410): revoked Token was revoked Mint a new one. Old token is permanently dead.
fetch /api/mcp/tools failed: 429 Bridge hit the rate-limit Slow client retries; or raise the cap in /api/mcp/tools/route.ts if real load
ECONNREFUSED localhost:3000 UCIP web isn't running ./scripts/start-dev.sh (dev) or docker compose up web (prod)
Tool returns { ok: false, error: '...' } Tool-layer failure (DB locked, missing data, etc.) Surface the error to the user; MCP client should re-prompt

Logs live in client stderr — Claude Desktop's ~/Library/Logs/Claude/mcp.log; Codex's ~/.codex/logs_*.sqlite.


Operator quick-prompts (try these in Claude / Codex)#

  • "Top 5 SKU bán chạy nhất 30 ngày qua trên Shopee, dùng UCIP."
  • "Tổng hợp GMV theo sàn quý này so với quý trước."
  • "Dự báo NMV 14 ngày tới."
  • "Vì sao GMV tuần này giảm? Phân tích contributors."
  • "Cho tao một bảng định nghĩa của AOV, contribution_margin, return_rate."

Architectural notes#

  • Why stdio first: every MCP client supports stdio out of the box; HTTP transport would need explicit configuration per client.
  • Why bridge + HTTPS (not direct DB): bridge sits on the operator's laptop; UCIP runs in prod. The bridge auths via Bearer token, NOT a database credential. No prod DB password leaves the prod boundary.
  • Why no LLM in the bridge: the client (Claude / Codex) IS the LLM. The bridge translates MCP requests → HTTP, returns JSON. Adding LLM there would mean two models in the loop.
  • Why this aligns with MCP standard: MCP is an open standard for agent-to-tool wiring, maintained under the Linux Foundation.

Files of interest#

Path Role
apps/mcp-server/src/index.ts The stdio bridge. Reads UCIP_API_* env, calls /api/mcp/*, speaks MCP wire to the client.
apps/web/src/app/api/mcp/tools/route.ts GET /api/mcp/tools — list catalog. Bearer auth, rate-limited, org-aware.
apps/web/src/app/api/mcp/exec/route.ts POST /api/mcp/exec — execute one tool. Writes one audit log per call.
apps/web/src/server/mcp/auth.ts Bearer token verification + lastUsedAt bump. Throws typed McpAuthError (401/403/410).
apps/web/src/server/mcp/exec.ts Tool catalog JSON Schema generation (via Zod v4 z.toJSONSchema) + executor (reuses CHAT_TOOL_DEFS).
apps/web/src/server/routers/admin.ts mintMcpToken / listMcpTokens / revokeMcpToken tRPC mutations (super-admin only).
apps/web/src/components/admin/org-drawer.tsx Org drawer → "MCP tokens" tab (super-admin UI for the lifecycle above).