# Step 5: MCP Tools MCP lets the model call third-party APIs (GitHub, Slack, Zoho Mail, Zendesk) on behalf of an end user. You get per-app scoped URLs, one per connected app, and choose which apps the model can see on each request. ``` ASSISTIV_API_BASE = https://api.assistiv.ai (Python API — inference, CRUD) ASSISTIV_MCP_BASE = https://mcp.assistiv.ai (MCP service — tool execution, OAuth) ``` --- ## How It Works ``` Phase 1 — Platform admin activates apps (one-time, on the dashboard) assistiv.ai/dashboard/mcp → Activate "GitHub" → paste GitHub OAuth Client ID + Secret → paste your platform's base URL (e.g. https://my-saas.com) → register ASSISTIV_MCP_BASE/oauth/callback in GitHub's OAuth app settings Phase 2 — End user connects apps (one-time per user per app, using sk-eu_*) End user clicks "Connect GitHub" on your site → your backend calls GET ASSISTIV_MCP_BASE/oauth/authorize?app=github → user approves at github.com → Assistiv stores the token encrypted → user lands back on {your_platform_url}/mcp/oauth-callback?status=connected Phase 3 — Get per-app MCP URLs (using sk-eu_*) GET ASSISTIV_API_BASE/v1/me/mcp-config → returns one scoped URL per connected app Phase 4 — Use the URLs for inference Option A: pass them as tools in POST ASSISTIV_API_BASE/v1/responses Option B: feed them into any MCP-compatible agent (Claude, Codex, Cursor, your own) ``` Every step after Phase 1 uses the end user's `sk-eu_*` key. The platform admin's only job is the one-time app activation. --- ## Phase 1: Activate Apps (Platform Admin) Done on the Assistiv dashboard. No API calls needed. 1. Go to `assistiv.ai/dashboard/mcp` 2. Click an app (e.g. GitHub) 3. Enter the OAuth Client ID and Client Secret from the provider's developer console 4. Enter your platform's base URL (where end users will be redirected after OAuth) 5. In the provider's OAuth app settings, set the callback URL to: `ASSISTIV_MCP_BASE/oauth/callback` Repeat for each app you want your end users to have access to. --- ## Phase 2: End User Connects Apps ### Discover which apps are available ``` GET ASSISTIV_MCP_BASE/apps Authorization: Bearer sk-eu_* ``` Returns the apps your platform has activated: ```json { "data": [{ "app_slug": "github" }, { "app_slug": "slack" }] } ``` ### Run the OAuth flow ```typescript // Your backend — kick off OAuth (browsers can't send Auth headers on navigations) const res = await fetch(`${ASSISTIV_MCP_BASE}/oauth/authorize?app=github`, { headers: { Authorization: `Bearer ${endUserKey}` }, redirect: "manual", }); const authorizeUrl = res.headers.get("location"); // Return authorizeUrl to your frontend → window.location.href = authorizeUrl ``` The user approves at the provider, then lands back on your site at: `{your_platform_url}/mcp/oauth-callback?app=github&status=connected` ### Check what's connected ``` GET ASSISTIV_MCP_BASE/connections Authorization: Bearer sk-eu_* ``` ```json { "data": [{ "id": "uuid", "appSlug": "github", "createdAt": "2026-04-12T10:30:00Z" }] } ``` ### Disconnect an app ``` DELETE ASSISTIV_MCP_BASE/connections/github Authorization: Bearer sk-eu_* → 204 ``` --- ## Phase 3: Get Per-App MCP URLs Once an end user has connected apps, fetch their scoped MCP config: ``` GET ASSISTIV_API_BASE/v1/me/mcp-config Authorization: Bearer sk-eu_* ``` Response: ```json { "standard": { "github": { "transport": "streamable_http", "url": "ASSISTIV_MCP_BASE/mcp/github", "headers": { "Authorization": "Bearer sk-eu_*" } }, "slack": { "transport": "streamable_http", "url": "ASSISTIV_MCP_BASE/mcp/slack", "headers": { "Authorization": "Bearer sk-eu_*" } } }, "openai": [ { "type": "mcp", "server_label": "github", "server_url": "ASSISTIV_MCP_BASE/mcp/github", "authorization": "Bearer sk-eu_*" }, { "type": "mcp", "server_label": "slack", "server_url": "ASSISTIV_MCP_BASE/mcp/slack", "authorization": "Bearer sk-eu_*" } ] } ``` Two formats in one response: - **`standard`** — standard MCP server config, works with any MCP-compatible agent or framework - **`openai`** — splice into the `tools` array of a `/v1/responses` request Each URL is scoped to one app. `ASSISTIV_MCP_BASE/mcp/github` only exposes GitHub's tools. `ASSISTIV_MCP_BASE/mcp/slack` only exposes Slack's. The end user's `sk-eu_*` key in the Authorization header identifies who they are. If the user has no connections, you get `standard: {}` and `openai: []`. --- ## Phase 4: Use the URLs ### Option A: Assistiv's Responses API (recommended) Pass per-app MCP tool items in your `/v1/responses` request. You pick which apps the model can see on each call. ```typescript const config = await fetch(`${ASSISTIV_API_BASE}/v1/me/mcp-config`, { headers: { Authorization: `Bearer ${endUserKey}` }, }).then(r => r.json()); // User only needs GitHub for this request — include just that one const res = await fetch(`${ASSISTIV_API_BASE}/v1/responses`, { method: "POST", headers: { Authorization: `Bearer ${endUserKey}`, "Content-Type": "application/json", }, body: JSON.stringify({ model: "", input: "Create a GitHub issue titled 'flaky test' in my-org/repo", tools: [config.openai.find(t => t.server_label === "github")], }), }); ``` Or include all connected apps: ```typescript tools: config.openai // model sees GitHub + Slack + everything connected ``` Or include multiple specific apps: ```typescript tools: config.openai.filter(t => ["github", "slack"].includes(t.server_label)) ``` The `server_label` is the app slug. The `server_url` is scoped per app. The `authorization` is the end user's key. That's it. #### OpenAI SDK compatible ```python from openai import OpenAI client = OpenAI(api_key=end_user_key, base_url=f"{ASSISTIV_API_BASE}/v1") response = client.responses.create( model="", input="Create a GitHub issue titled 'hello' in my-org/test-repo", tools=[ { "type": "mcp", "server_label": "github", "server_url": f"{ASSISTIV_MCP_BASE}/mcp/github", "authorization": f"Bearer {end_user_key}", "require_approval": "never", } ], ) ``` `require_approval` must be `"never"` (or omitted). v1 does not support human-in-the-loop approval. Some OpenAI SDK examples default to `"always"`, which returns `422`. #### Tool item fields | Field | Required | Value | |-------|----------|-------| | `type` | yes | `"mcp"` | | `server_label` | yes | App slug: `"github"`, `"slack"`, `"zoho_mail"`, `"zendesk"` | | `server_url` | yes | `ASSISTIV_MCP_BASE/mcp/{app_slug}` | | `authorization` | yes | `"Bearer sk-eu_*"` | | `require_approval` | no | `"never"` if set (only valid value in v1) | #### Response shape (non-streaming) ```json { "id": "resp_abc", "object": "response", "status": "completed", "model": "", "output": [ { "type": "mcp_call", "id": "mcp_xyz", "server_label": "github", "name": "github_create_issue", "arguments": "{\"owner\":\"my-org\",\"repo\":\"test-repo\",\"title\":\"hello\"}", "output": "Issue #42 created", "status": "completed" }, { "type": "message", "id": "msg_abc", "role": "assistant", "content": [{ "type": "output_text", "text": "Done — created issue #42." }] } ], "usage": { "input_tokens": 120, "output_tokens": 35, "total_tokens": 155 } } ``` The `server_label` in `mcp_call` items tells you which app handled the tool. #### Streaming events Set `stream: true`. The stream emits: ``` response.created response.mcp_list_tools.in_progress response.mcp_list_tools.completed ← discovered tools for included apps response.output_item.added ← mcp_call item response.mcp_call_arguments.delta response.mcp_call_arguments.done response.mcp_call.in_progress response.mcp_call.completed ← tool returned response.output_item.done response.output_item.added ← assistant message response.output_text.delta ← final answer streams response.output_text.done response.output_item.done response.completed ``` #### Hybrid execution (MCP + function tools) Pass both in the same `tools` array: ```json { "tools": [ { "type": "function", "function": { "name": "get_internal_data", "..." } }, { "type": "mcp", "server_label": "github", "..." } ] } ``` Rules: 1. If the model returns only MCP tool calls, Assistiv executes them and loops. 2. If the model returns any function tool call, the loop returns immediately so your platform can handle it. MCP calls in the same turn are not executed. 3. Max loop depth: 10 iterations. 4. Each turn debits the wallet/budget normally. #### Error responses | Code | Meaning | |------|---------| | `503 mcp_unreachable` | MCP server or tool list unreachable | | `422` | Validation failed (missing field, bad require_approval) | | `500 max_iterations_exceeded` | Loop didn't converge in 10 iterations | | `402 payment_required` | Wallet/budget drained mid-loop | --- ### Option B: Any MCP-Compatible Agent (direct MCP config) The `standard` format from `/v1/me/mcp-config` is the standardized MCP server config. It works with any agent or tool that speaks the MCP protocol: Claude Desktop, OpenAI Codex, Cursor, your own agent harness, or anything that accepts `streamable_http` MCP server configs. Each key in `standard` is an app slug. Each value is a complete MCP server config with `transport`, `url`, and `headers`. Pass whichever subset you want. #### Fetch the config ```bash curl ASSISTIV_API_BASE/v1/me/mcp-config \ -H "Authorization: Bearer sk-eu_*" ``` The `standard` field in the response: ```json { "github": { "transport": "streamable_http", "url": "ASSISTIV_MCP_BASE/mcp/github", "headers": { "Authorization": "Bearer sk-eu_*" } }, "slack": { "transport": "streamable_http", "url": "ASSISTIV_MCP_BASE/mcp/slack", "headers": { "Authorization": "Bearer sk-eu_*" } } } ``` #### Claude Desktop / claude_desktop_config.json ```json { "mcpServers": { "github": { "transport": "streamable_http", "url": "ASSISTIV_MCP_BASE/mcp/github", "headers": { "Authorization": "Bearer sk-eu_*" } } } } ``` #### OpenAI Codex CLI ```bash codex --mcp-config '{"github": {"transport": "streamable_http", "url": "ASSISTIV_MCP_BASE/mcp/github", "headers": {"Authorization": "Bearer sk-eu_*"}}}' ``` #### Python (langchain-mcp-adapters) ```python from langchain_mcp_adapters.client import MultiServerMCPClient config = requests.get( f"{ASSISTIV_API_BASE}/v1/me/mcp-config", headers={"Authorization": f"Bearer {end_user_key}"}, ).json() # All connected apps client = MultiServerMCPClient(config["standard"]) all_tools = await client.get_tools() # Just GitHub client = MultiServerMCPClient({"github": config["standard"]["github"]}) github_tools = await client.get_tools() ``` #### Any MCP client The URL pattern is always `ASSISTIV_MCP_BASE/mcp/{app_slug}` with the end user's `sk-eu_*` key in the Authorization header. Any tool or framework that can connect to an MCP server over streamable HTTP works out of the box. --- ## OAuth Flow Details ### The redirect chain ``` {your_platform_url}/integrations (user clicks "Connect GitHub") → github.com/login/oauth/authorize (user approves) → ASSISTIV_MCP_BASE/oauth/callback (sub-second, token exchange) → {your_platform_url}/mcp/oauth-callback?app=github&status=connected ``` ### Your /mcp/oauth-callback route Query parameters on arrival: | Param | Type | Meaning | |-------|------|---------| | `app` | string | App slug (e.g. `github`) | | `status` | `"connected"` or `"error"` | Outcome | | `error` | string | Error code when `status=error` | | `error_description` | string | Human-readable detail | Error codes: `access_denied`, `token_exchange_failed`, `no_access_token`, `missing_oauth_credentials`, `unknown_app`, `internal_error`. ### Common gotchas - **Callback URL must match exactly** in the provider's OAuth app settings, including protocol and trailing slash. - **Browsers can't send Authorization headers on navigations.** Always call `GET ASSISTIV_MCP_BASE/oauth/authorize` from your backend, extract the 302 Location, and redirect the browser client-side. - **`platform_app_configs.redirect_url` is your platform's URL**, not the OAuth callback URL. They're different things. ### Testing OAuth locally Register a separate dev OAuth app on the provider side with callback URL `http://localhost:4000/oauth/callback`. Use those credentials for your dev platform's app config. Production OAuth app stays untouched. --- ## MCP Free Tier First 100 MCP tool calls per platform per calendar month are free. No wallet debit, no log row. After the 100th call, per-tool pricing applies (configured in `tool_pricing`). Counter resets on the 1st of each month. --- ## Tool Name Convention Tool names come from vendored Pipedream component action keys with dashes converted to underscores: - `github_create_issue` - `slack_send_message` - `zoho_mail_send_email` - `zendesk_create_ticket` Match `tools/list` output verbatim in prompts. --- ## Reference Endpoints | Method | Endpoint | Auth | Description | |--------|----------|------|-------------| | GET | `ASSISTIV_MCP_BASE/apps` | `sk-eu_*` | List apps platform has activated | | GET | `ASSISTIV_MCP_BASE/connections` | `sk-eu_*` | List end user's connected apps | | DELETE | `ASSISTIV_MCP_BASE/connections/{app}` | `sk-eu_*` | Disconnect an app | | GET | `ASSISTIV_MCP_BASE/oauth/authorize?app={slug}` | `sk-eu_*` | Start OAuth flow (returns 302) | | POST | `ASSISTIV_MCP_BASE/mcp/{app_slug}` | `sk-eu_*` | Per-app MCP protocol endpoint | | GET | `ASSISTIV_API_BASE/v1/me/mcp-config` | `sk-eu_*` | Get per-app MCP URLs | | POST | `ASSISTIV_API_BASE/v1/responses` | `sk-eu_*` | Inference with MCP tools | --- Next: [Step 6 — Monitor & Operate](https://www.assistiv.ai/docs/integration/step-6-monitor-and-operate.txt)