Iris ↔ KernelService MCP Integration
KernelService exposes a curated subset of tenant capabilities to partner AI clients (Iris first) over the Model Context Protocol using Streamable HTTP transport, gated by OAuth 2.1 with PKCE.
This page is the integration guide for the Iris team and any future MCP client targeting KernelService.
Endpoints
All endpoints are under https://kernelservice.com.
| Purpose | URL |
|---|---|
| Authorization-server metadata (RFC 8414) | /.well-known/oauth-authorization-server |
| Authorize (user consent) | /oauth/authorize |
| Token exchange | /oauth/token |
| Token revocation (RFC 7009) | /oauth/revoke |
| MCP endpoint (Streamable HTTP) | /mcp |
Clients should fetch the metadata document first and discover all other endpoints from it. The published values are authoritative — do not hardcode them.
OAuth client
Iris is pre-registered as a public OAuth client. PKCE is mandatory; there is no client secret.
client_id: irisredirect_uris: [ to be supplied by the Iris team ]allowed_scopes: mcp.read, mcp.draftTo register a new partner client, add a row to oauth_clients (the
seed in kernel.blueprints.seed.seed_oauth_clients is the canonical
example).
Scopes
| Scope | What it allows |
|---|---|
mcp.read | Read-only tools (list emails, list pending approvals) |
mcp.draft | Tools that produce drafts. Drafts always land in the user’s existing KernelService approval queue — nothing is ever sent without explicit human approval |
mcp.approve is not offered in v1 — approval happens through
KernelService’s existing UI, not the partner client. If you have a use
case that needs it, raise with the KernelService team before assuming
it’s coming.
Authorization flow
Standard OAuth 2.1 authorization-code with PKCE. The user must already have a KernelService account before authorizing — there is no signup inside the OAuth flow.
┌──────┐ ┌────────────────┐│ Iris │ GET /oauth/authorize? │ KernelService ││ │ ──► response_type=code │ ││ │ client_id=iris │ ││ │ redirect_uri=… │ ││ │ scope=mcp.read+mcp.draft ││ │ state=… │ ││ │ code_challenge=… │ ││ │ code_challenge_method=S256 ││ │ │ ││ │ ◄── 302 redirect_uri? │ ││ │ code=… │ ││ │ state=… │ ││ │ │ ││ │ POST /oauth/token │ ││ │ ──► grant_type=authorization_code ││ │ code=… │ ││ │ redirect_uri=… │ ││ │ client_id=iris │ ││ │ code_verifier=… │ ││ │ │ ││ │ ◄── {access_token, refresh_token, ││ │ expires_in: 3600, │ ││ │ token_type: Bearer, │ ││ │ scope: "mcp.read mcp.draft"} │└──────┘ └────────────────┘Access tokens expire in 1 hour. Refresh tokens expire in 30 days and are rotated on every use — present an already-used refresh token and the entire token family is revoked.
MCP transport
The MCP endpoint at /mcp uses Streamable HTTP.
Send the access token as Authorization: Bearer <token> on every
request.
Stateless mode — each tool call is independent, no session state is maintained server-side between requests.
Tools (v1)
ping
Verify the connection. No side effects.
| Arg | Type | Notes |
|---|---|---|
| none |
Returns:
{ "ok": true, "tenant_id": "5e9c…", "client_id": "iris", "scopes": ["mcp.read", "mcp.draft"]}list_recent_customer_emails
Scope: mcp.read.
| Arg | Type | Default | Notes |
|---|---|---|---|
limit | int | 20 | 1–50 |
Returns up to limit recent emails from the tenant’s connected
mailbox (Gmail or Outlook), normalised to a single shape:
{ "emails": [ { "id": "19c8…", "thread_id": "19c8…", "subject": "Question about my order", "body_preview": "Hi — I was wondering…", "received_at": "Wed, 19 May 2026 14:23:00 +1000", "channel": "gmail" } ], "count": 1, "channel": "gmail"}If no mailbox is connected: error code no_provider_connected.
draft_customer_response
Scope: mcp.draft. Never sends. Creates an approval-queue entry
the tenant admin approves from the KS approvals UI.
| Arg | Type | Default | Notes |
|---|---|---|---|
message_id | string | — | Required. The id from list_recent_customer_emails |
draft_text | string | — | Required. Plain text — KS wraps it in safe HTML |
tone | string | "matching" | Optional metadata: formal, friendly, matching. Surfaced in the approval row only — does not transform the draft |
attach_files | string[] | null | Reserved for v0.2 — currently ignored |
KS fetches the original message from the connected provider to
populate the recipient address — the client cannot specify the
to address. This means a compromised partner client cannot
redirect drafts to arbitrary recipients.
Returns:
{ "draft_id": "8a3f…", "status": "awaiting_approval", "preview_url": "https://kernelservice.com/review?approval=8a3f…"}list_pending_approvals
Scope: mcp.read.
| Arg | Type | Default | Notes |
|---|---|---|---|
limit | int | 20 | 1–100 |
Returns the tenant’s currently-pending approvals — including both Iris-originated drafts and ones queued by KS’s own agents:
{ "approvals": [ { "id": "8a3f…", "type": "email_send", "created_at": "2026-05-19T04:23:11+00:00", "action_url": "https://kernelservice.com/review?approval=8a3f…", "source": "mcp", "oauth_client_id": "iris" } ], "count": 1}Rate limits
Per access token:
| Window | Limit |
|---|---|
| 60 seconds | 60 calls |
| 24 hours | 10,000 calls |
Exceeded calls return error code rate_limited. The audit row is
still written so abuse is visible.
Error codes
Tools surface structured errors via the MCP error envelope. The
relevant code values:
| Code | Meaning |
|---|---|
insufficient_scope | Token does not grant the required scope |
invalid_argument | Argument failed validation (e.g. limit out of range) |
no_provider_connected | Tenant has no mailbox provider wired up |
provider_error | Underlying Gmail/Outlook call failed |
auth_failed | Provider OAuth tokens are missing or expired |
message_not_found | The message_id does not exist in the tenant’s mailbox |
missing_recipient | Could not determine the original sender to reply to |
rate_limited | Per-token rate limit hit |
Revocation
Either side can revoke at any time:
- Partner side:
POST /oauth/revokewith the access or refresh token. Revoking a refresh token also revokes all tokens that were rotated from it. - Tenant admin:
POST /api/v1/tenant/iris/disable(or the toggle in Workspace settings). This setstenant.iris_enabled = falseAND revokes every active token belonging to the tenant. The next MCP call from any of the tenant’s existing tokens returns401.
The tenant flag is re-checked on every MCP request, so disabling is effectively instant — there is no in-flight session that survives.
Audit
Every MCP tool call writes one row to mcp_tool_calls:
| Column | Value |
|---|---|
tenant_id | The tenant the token was scoped to |
oauth_client_id | e.g. iris |
oauth_access_token_id | Link back to the token row |
scope_used | The scope the wrapper required |
tool_name | e.g. draft_customer_response |
args | The tool args (string values >2000 chars truncated) |
status | success, error, or rate_limited |
error | Error message if any |
latency_ms | Wall time |
created_at | Timestamp |