Skip to content

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.

PurposeURL
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: iris
redirect_uris: [ to be supplied by the Iris team ]
allowed_scopes: mcp.read, mcp.draft

To 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

ScopeWhat it allows
mcp.readRead-only tools (list emails, list pending approvals)
mcp.draftTools 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.

ArgTypeNotes
none

Returns:

{
"ok": true,
"tenant_id": "5e9c…",
"client_id": "iris",
"scopes": ["mcp.read", "mcp.draft"]
}

list_recent_customer_emails

Scope: mcp.read.

ArgTypeDefaultNotes
limitint201–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…",
"from": "[email protected]",
"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.

ArgTypeDefaultNotes
message_idstring—Required. The id from list_recent_customer_emails
draft_textstring—Required. Plain text — KS wraps it in safe HTML
tonestring"matching"Optional metadata: formal, friendly, matching. Surfaced in the approval row only — does not transform the draft
attach_filesstring[]nullReserved 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.

ArgTypeDefaultNotes
limitint201–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",
"summary": "Email to [email protected]: Re: Question about my order",
"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:

WindowLimit
60 seconds60 calls
24 hours10,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:

CodeMeaning
insufficient_scopeToken does not grant the required scope
invalid_argumentArgument failed validation (e.g. limit out of range)
no_provider_connectedTenant has no mailbox provider wired up
provider_errorUnderlying Gmail/Outlook call failed
auth_failedProvider OAuth tokens are missing or expired
message_not_foundThe message_id does not exist in the tenant’s mailbox
missing_recipientCould not determine the original sender to reply to
rate_limitedPer-token rate limit hit

Revocation

Either side can revoke at any time:

  • Partner side: POST /oauth/revoke with 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 sets tenant.iris_enabled = false AND revokes every active token belonging to the tenant. The next MCP call from any of the tenant’s existing tokens returns 401.

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:

ColumnValue
tenant_idThe tenant the token was scoped to
oauth_client_ide.g. iris
oauth_access_token_idLink back to the token row
scope_usedThe scope the wrapper required
tool_namee.g. draft_customer_response
argsThe tool args (string values >2000 chars truncated)
statussuccess, error, or rate_limited
errorError message if any
latency_msWall time
created_atTimestamp