Connect OpenClaw, Hermes, and Agent Tools to the CorpusIQ MCP Server
By CorpusIQ
CLI tools, agent loops, and CI runners cannot pop a browser to complete a standard OAuth redirect. The CorpusIQ MCP server supports the OAuth 2.0 Device Authorization Grant (RFC 8628) so OpenClaw, Hermes agents, and any script-based tool can authenticate without embedding a client secret or running a redirect server. The user signs in once; the tool receives a signed JWT and uses it on every subsequent MCP call.
When to use device login
Use device login when your tool runs outside a normal browser (terminal, daemon, CI runner, IDE plugin, agent loop), you do not want to store or embed an OAuth client secret, and you want the user to authenticate so connector access is scoped to their own CorpusIQ account.
If your tool is a standard web app, use the regular OAuth PKCE redirect flow instead. Device login is specifically for non-interactive environments.
Server endpoints
The production base URL is https://mcp2.corpusiq.io. A development environment is available at https://mcp-dev.corpusiq.io. Three endpoints are used by the device flow:
| Endpoint | Method | Purpose |
|---|---|---|
| /oauth/device/authorize | POST | Issue device code and user code |
| /oauth/device/verify | GET / POST | Browser page where the user enters the user code |
| /oauth/token | POST | Poll for the access token (JWT) |
| /mcp | POST | MCP endpoint, called with Authorization: Bearer |
The four-step flow
The device flow has four steps. Your tool handles steps 1, 2, and 4; the user handles step 3.
Step 1. Request a device code
POST to /oauth/device/authorize with no body. The response contains a device_code (opaque, never shown to the user), a user_code (8 alphanumeric characters in XXXX-XXXX form), an expiry of 900 seconds, and a minimum polling interval of 5 seconds.
curl -sS -X POST https://mcp2.corpusiq.io/oauth/device/authorize
# Response
{
"device_code": "kS3...long opaque string...",
"user_code": "ABCD-EFGH",
"verification_uri": "https://mcp2.corpusiq.io/oauth/device/verify",
"verification_uri_complete": "https://mcp2.corpusiq.io/oauth/device/verify?code=ABCD-EFGH",
"expires_in": 900,
"interval": 5
}Step 2. Show the user where to go
Display both the verification URI and the user code. Optionally print the pre-filled URL as well. The user opens it on any device, including a phone.
Sign in to CorpusIQ to authorize this tool: Open: https://mcp2.corpusiq.io/oauth/device/verify Code: ABCD-EFGH Or visit: https://mcp2.corpusiq.io/oauth/device/verify?code=ABCD-EFGH
Step 3. User authenticates (nothing for your tool to do)
The user opens the verification URI, signs in with their CorpusIQ account, and the server links the resulting JWT to the device code your tool holds. Your tool waits.
Step 4. Poll for the token
Poll /oauth/token no more often than the interval from step 1. You will receive authorization_pending until the user completes sign-in, then a JWT once they do. Honor the interval exactly; excessive polling may return slow_down.
curl -sS -X POST https://mcp2.corpusiq.io/oauth/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=urn:ietf:params:oauth:grant-type:device_code" \
-d "device_code=kS3...long opaque string..."
# Success response
{
"access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
"token_type": "bearer",
"expires_in": 3540
}Use the JWT as a Bearer token on every MCP call:
curl -X POST https://mcp2.corpusiq.io/mcp \
-H "Authorization: Bearer eyJhbGciOiJSUzI1NiIsI..." \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'Reference implementation in Python
Drop this into any tool or agent harness. No dependencies beyond requests.
# device_login.py -- CorpusIQ MCP device-login helper
from __future__ import annotations
import sys, time
from typing import Any
import requests
DEVICE_GRANT = "urn:ietf:params:oauth:grant-type:device_code"
class DeviceLoginError(RuntimeError):
pass
def login(base_url: str = "https://mcp2.corpusiq.io", timeout: int = 30) -> dict[str, Any]:
s = requests.Session()
r = s.post(f"{base_url}/oauth/device/authorize", timeout=timeout)
r.raise_for_status()
dc = r.json()
print(
f"\nSign in to CorpusIQ:\n"
f" Open: {dc['verification_uri']}\n"
f" Code: {dc['user_code']}\n"
f" Or visit: {dc['verification_uri_complete']}\n",
flush=True,
)
interval = int(dc.get("interval", 5))
deadline = time.time() + int(dc.get("expires_in", 900))
while time.time() < deadline:
time.sleep(interval)
tr = s.post(
f"{base_url}/oauth/token",
data={"grant_type": DEVICE_GRANT, "device_code": dc["device_code"]},
timeout=timeout,
)
if tr.status_code == 200:
return tr.json()
body = tr.json() if tr.headers.get("content-type", "").startswith("application/json") else {}
err = body.get("error", "")
if err == "authorization_pending":
continue
if err == "slow_down":
interval += 5
continue
if err in ("expired_token", "invalid_grant"):
raise DeviceLoginError(f"Device login failed: {err}")
raise DeviceLoginError(f"Unexpected response {tr.status_code}: {tr.text}")
raise DeviceLoginError("Device code expired before the user authorized the tool.")
if __name__ == "__main__":
try:
token = login()
except DeviceLoginError as exc:
print(str(exc), file=sys.stderr)
sys.exit(1)
print(token["access_token"])Reference implementation in Node.js / TypeScript
// device-login.ts -- CorpusIQ MCP device-login helper
const DEVICE_GRANT = "urn:ietf:params:oauth:grant-type:device_code";
export async function deviceLogin(
baseUrl = "https://mcp2.corpusiq.io",
): Promise<{ access_token: string; expires_in: number }> {
const a = await fetch(`${baseUrl}/oauth/device/authorize`, { method: "POST" });
if (!a.ok) throw new Error(`authorize failed: ${a.status}`);
const dc = await a.json();
console.error(
`\nSign in to CorpusIQ:\n Open: ${dc.verification_uri}` +
`\n Code: ${dc.user_code}\n Or visit: ${dc.verification_uri_complete}\n`,
);
let interval = Number(dc.interval ?? 5);
const deadline = Date.now() + Number(dc.expires_in ?? 900) * 1000;
while (Date.now() < deadline) {
await new Promise((r) => setTimeout(r, interval * 1000));
const body = new URLSearchParams({ grant_type: DEVICE_GRANT, device_code: dc.device_code });
const t = await fetch(`${baseUrl}/oauth/token`, {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body,
});
if (t.ok) return t.json();
const err = (await t.json().catch(() => ({})))?.error;
if (err === "authorization_pending") continue;
if (err === "slow_down") { interval += 5; continue; }
if (err === "expired_token" || err === "invalid_grant") throw new Error(err);
throw new Error(`token endpoint returned ${t.status}`);
}
throw new Error("device code expired");
}Connecting OpenClaw
OpenClaw treats the CorpusIQ MCP server as a standard streamable-HTTP endpoint with a Bearer token. Obtain the JWT once with the device-login helper, then configure OpenClaw to use it. Re-run the helper when the token nears expiry.
# 1. Obtain the JWT
JWT=$(python device_login.py)
export CORPUSIQ_JWT="$JWT"
# 2. OpenClaw MCP server configuration (YAML)
mcp_servers:
corpusiq:
transport: streamable-http
url: https://mcp2.corpusiq.io/mcp
headers:
Authorization: "Bearer ${CORPUSIQ_JWT}"
# 3. Launch OpenClaw with the JWT in the environment
openclaw runThe JWT has no refresh token. When it expires, re-run python device_login.py, update the environment variable, and restart OpenClaw. Connector authorizations inside CorpusIQ persist across re-authentications so the user does not re-link anything.
Connecting Hermes agents
Hermes agents launch in non-interactive contexts and cannot open a browser mid-run. Run device login once during the bootstrap script, push the JWT into a short-lived secret store, and have each agent step read from there.
# bootstrap.sh -- runs once per session, interactive terminal set -euo pipefail JWT=$(python device_login.py) # Push into GitHub Actions environment echo "CORPUSIQ_JWT=$JWT" >> "$GITHUB_ENV" # Or push into Azure Key Vault for longer-running agents az keyvault secret set \ --vault-name corpus-hermes-kv \ --name "hermes-mcp-jwt-$(date +%Y%m%d-%H)" \ --value "$JWT" >/dev/null
Each Hermes agent step then reads the JWT from the environment and passes it on the MCP transport:
import os
from mcp.client.streamable_http import streamablehttp_client
headers = {"Authorization": f"Bearer {os.environ['CORPUSIQ_JWT']}"}
async with streamablehttp_client("https://mcp2.corpusiq.io/mcp", headers=headers) as (r, w, _):
# issue tool calls as normal
...Generic MCP client SDK (any language)
Any MCP client SDK that supports a custom HTTP headers hook on its streamable-HTTP transport works with the CorpusIQ server. Set one header:
Authorization: Bearer <jwt-from-device-login>
That is the only authentication the server requires. The Accept header must include both application/json and text/event-stream or the server returns a 406 Not Acceptable. Most MCP SDKs set both automatically on streamable-HTTP transports.
Error responses while polling
The token endpoint follows RFC 8628 exactly. Handle these four error codes:
| Error | Meaning | Action |
|---|---|---|
| authorization_pending | User has not signed in yet | Sleep for interval and retry |
| slow_down | Polling too fast | Add 5s to interval, then retry |
| expired_token | device_code exceeded expires_in | Restart the device flow |
| invalid_grant | Unknown or consumed device_code | Restart the device flow |
Token lifetime and security
The JWT lifetime is reported in expires_in at issuance and encoded in its exp claim. There is no refresh token. Re-run the device flow when the token is within a few minutes of expiry.
Treat the JWT like a password: never log it, never echo it to an uncontrolled terminal, never commit it to source control. Store it in an OS keychain, a secret manager, or an encrypted file with 0600 permissions. The device_code is also sensitive until exchanged; once exchanged the server deletes it.
What tools get access to after connecting
Once authenticated, the tool sees the same 37+ read-only business tool surfaces the user has authorized in their CorpusIQ account: QuickBooks, Shopify, HubSpot, Gmail, Google Drive, GA4, Google Ads, Meta Ads, Slack, PostgreSQL, SQL Server, and more. Every tool call is read-only OAuth. Nothing can be written, sent, modified, or deleted. CorpusIQ is CASA Tier 2 certified and stores zero customer data; records are read on demand and released after the answer returns.
Common mistakes
- Polling faster than the interval. The server may return slow_down and you will burn time adding 5s to each poll cycle. Match the interval exactly.
- Missing the Accept header. Requests without both
application/jsonandtext/event-streamin the Accept header receive a 406 Not Acceptable. Most MCP SDKs handle this automatically for streamable-HTTP transports. - Storing the JWT in a plain environment variable that persists in shell history. Use a secret manager for anything longer-lived than a single terminal session.
- Forgetting to re-run device login when the JWT expires. The server returns 401; check the exp claim before each session rather than waiting for a failure.
- Calling the wrong path. The MCP endpoint is
POST /mcp, notGET /mcpor/api/mcp.
If you do not want to build a helper
The two reference implementations above (Python and TypeScript) are self-contained and require no dependencies beyond the standard library plus one HTTP client. Copy them into your project and adjust the base URL if you are testing against the development environment.
If your stack is neither Python nor TypeScript, any HTTP client that handles form-encoded POST bodies and JSON responses works. The protocol is pure HTTP; there is nothing language-specific about it.
Related reading
- Building an MCP Server: A Practical Guide
- MCP Security: Protecting Your Data in the Context Window
- MCP server for Claude
- MCP server for ChatGPT
- See all 37+ live CorpusIQ connectors
- Pricing, starting at $29.95 per month
Frequently asked questions
The CorpusIQ MCP server implements the OAuth 2.0 Device Authorization Grant (RFC 8628). A CLI or agent tool requests a short alphanumeric user code, the user signs in once in a browser, and the tool receives a signed JWT it uses as a Bearer token on every subsequent MCP call. No client secret is embedded in the tool.
Run the device-login helper to obtain a JWT, export it as CORPUSIQ_JWT, then configure OpenClaw's MCP server block with transport: streamable-http, url: https://mcp2.corpusiq.io/mcp, and Authorization: Bearer ${CORPUSIQ_JWT} in the headers. Re-run the helper when the JWT nears its expiry.
Hermes agents run the device-login helper once during bootstrap, then push the resulting JWT into a short-lived secret store such as Azure Key Vault or a GitHub Actions secret. Each agent step reads the JWT from the environment and passes it as Authorization: Bearer on the MCP streamable-HTTP transport. No browser is needed after the initial bootstrap run.
Yes. Once a user links a connector (Google, Microsoft, Shopify, QuickBooks, and so on) to their CorpusIQ account, those connector tokens stay attached to their identity. A new JWT obtained by device login immediately has access to the same connectors. The user does not re-link anything when the JWT expires.
The JWT's lifetime is reported in the expires_in field of the token response and encoded in its exp claim. The device-code grant does not issue a refresh token by design: re-authenticating the user is the intent. Tools should re-run the device flow when the token is within a few minutes of expiry.
The production endpoint is https://mcp2.corpusiq.io/mcp. It accepts POST requests with Content-Type: application/json and Accept: application/json, text/event-stream, plus Authorization: Bearer <jwt>. The transport is streamable HTTP, compatible with any MCP client SDK that supports remote HTTP servers.
