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 MCPtools/list+tools/callrequests 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
- Log in with a super-admin account (seeded accounts:
scripts/CREDENTIALS.md) →/admin/orgs→ click the org you want to expose. - In the drawer, switch to the "MCP tokens" tab.
- Fill in:
- Label — who is this for? e.g.
ops@younetgroup.com — Claude Desktop. - TTL (days) — default 90.
- Click Mint token. Copy the green-banner token NOW (
yngmcp_<base64url>) — it's shown once, hashed at rest after. - 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#
- Token issuance is super-admin only.
admin.mintMcpTokenis gated bysuperAdminProcedure. Org admins cannot self-mint. Audit trail inaudit_logs(admin.mcp_token.mint). - Tokens are hashed at rest (sha256). Cleartext is shown ONCE in the mint dialog — never retrievable after.
- 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.
- Rate-limited. 30 req/min per IP at the bridge, mirroring the chat surface. 60 req/min on the catalog endpoint (rarely-changing).
- RLS preserved. Every tool call pins
setOrgContext(token.orgId)before hitting Postgres. A token for org A can never see org B's data. - No write tools. Today the catalog is read-only (querying metrics). Adding a write tool would require a separate review.
- Audit log per call.
audit_logsrow 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. - 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). |