BONKETIBA MCP server
Connect ChatGPT, Claude, Codex, Cursor or any MCP-compatible client to your Bonketiba workspace. Run AI searches with cited sources, and manage notes, bookmarks and search history — all as the signed-in user.
Endpoint
Streamable HTTP: https://<your-bonketiba-domain>/mcp
OAuth metadata: https://<your-bonketiba-domain>/.well-known/oauth-protected-resourceTransport: MCP Streamable HTTP (spec 2025-06-18). Requests must set Accept: application/json, text/event-stream.
Authentication
OAuth 2.1 with dynamic client registration, backed by the Bonketiba authorization server. Well-behaved MCP clients discover, register and complete consent automatically — the caller signs in as a Bonketiba user and every tool runs under Row Level Security as that user.
- Client fetches
/.well-known/oauth-protected-resource. - Client registers dynamically and starts an authorization flow.
- User approves on the Bonketiba consent screen.
- Client receives an access token bound to that user.
Scopes
Each tool is annotated with one of four scopes. By default an authorized client can call every tool; you can restrict a specific client by inserting a row in mcp_client_scopes from your Workspace with the exact scopes you want to allow.
Rate limits & audit
- Per-user cap: 60 MCP tool calls / minute. Excess calls return an
isErrorwith a retry hint. - Every call — success, error, denied, rate-limited — is written to
mcp_audit_logwith the tool name, client id, outcome and a compact input summary. - Users see only their own audit rows (RLS). Export from your Workspace.
Tools
run_ai_searchscope: searchRun a Bonketiba AI reasoning search. Returns a structured answer with checklist, cost, timeframe, operational steps, and cited sources with reliability scores.
query (string, 3–280 chars, required)— The user's search query in any supported language.persist (boolean, optional)— Save the query in the caller's search history (default true).
structuredContent.answer: { summary, cost, timeframe, checklist[], steps[{title,detail}], sources[{name,url,stance,excerpt,reliability}], comparison }
{
"name": "run_ai_search",
"arguments": { "query": "How to open a SRLS company in Italy?" }
}list_search_historyscope: historyList the signed-in user's recent AI search queries.
limit (int 1–100, optional)— Max entries (default 20).
structuredContent.items: [{ id, query, model, created_at }]
{ "name": "list_search_history", "arguments": { "limit": 10 } }delete_historyscope: historydestructiveDelete one search history entry.
id (uuid, required)— History entry id.
content: 'Deleted <id>'
{ "name": "delete_history", "arguments": { "id": "…uuid…" } }list_saved_sourcesscope: sourcesList bookmarked sources.
limit (int 1–200, optional)— Max sources (default 50).
structuredContent.items: [{ id, url, title, note, created_at }]
{ "name": "list_saved_sources", "arguments": {} }save_sourcescope: sourcesBookmark a source URL in the Workspace.
url (URL, required)— Source URL.title (string, optional)— Display title.note (string, optional)— Personal note.
structuredContent.source: { id, url, title, note, created_at }
{ "name": "save_source", "arguments": { "url": "https://example.com", "title": "Doc" } }delete_sourcescope: sourcesdestructiveRemove a bookmarked source.
id (uuid, required)— Saved source id.
content: 'Deleted <id>'
{ "name": "delete_source", "arguments": { "id": "…uuid…" } }list_notesscope: notesList Workspace notes, newest first.
limit (int 1–200, optional)— Max notes (default 50).
structuredContent.items: [{ id, title, content, updated_at, created_at }]
{ "name": "list_notes", "arguments": { "limit": 20 } }create_notescope: notesCreate a note in the Workspace.
title (string, ≥1 char, required)— Note title.content (string, optional)— Note body (markdown allowed).
structuredContent.note: { id, title, content, created_at }
{ "name": "create_note", "arguments": { "title": "Idea", "content": "…" } }update_notescope: notesUpdate a note's title and/or content.
id (uuid, required)— Note id.title (string, optional)— New title.content (string, optional)— New body.
structuredContent.note: { id, title, content, updated_at }
{ "name": "update_note", "arguments": { "id": "…", "content": "Updated" } }delete_notescope: notesdestructiveDelete a Workspace note.
id (uuid, required)— Note id.
content: 'Deleted <id>'
{ "name": "delete_note", "arguments": { "id": "…uuid…" } }Connecting clients
In the MCP connectors panel, add a server with URL https://<your-bonketiba-domain>/mcp. Complete OAuth consent when prompted.
Add an HTTP MCP server pointing at the same URL. The client will fetch OAuth metadata and register dynamically.
Health check
GET /api/public/mcp-health returns tool availability, the configured rate-limit, and error / denial / rate-limit counts from the last hour. Useful for uptime monitors and status dashboards.
curl https://<your-bonketiba-domain>/api/public/mcp-healthSandbox
Signed-in users can invoke every tool with a live OAuth token and inspect the raw JSON-RPC request/response at /mcp-sandbox. Admins listed in MCP_ADMIN_USER_IDS also see an audit-log CSV export panel there.
Code examples
Minimal, copy-pasteable clients. Both use the official MCP SDK's HTTP transport, which handles OAuth 2.1 discovery, dynamic registration and token refresh automatically.
// npm i @modelcontextprotocol/sdk
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
const transport = new StreamableHTTPClientTransport(
new URL("https://<your-bonketiba-domain>/mcp"),
// The SDK handles OAuth 2.1 discovery + dynamic registration
// via /.well-known/oauth-protected-resource. On first run it opens
// a browser tab for consent, then caches tokens locally.
{ authProvider: /* your OAuth persistence, e.g. keychain-backed */ undefined }
);
const client = new Client({ name: "my-agent", version: "1.0.0" });
await client.connect(transport);
// 1) Discover tools (10 available; scopes: search | history | sources | notes)
const { tools } = await client.listTools();
console.log(tools.map((t) => t.name));
// 2) Run an AI search (scope: search)
const search = await client.callTool({
name: "run_ai_search",
arguments: { query: "How to open a company in Rwanda?" },
});
console.log(search.structuredContent);
// 3) Save a source (scope: sources)
await client.callTool({
name: "save_source",
arguments: { url: "https://gov.rwanda.rw", title: "Rwanda gov" },
});
// 4) Create a note (scope: notes)
await client.callTool({
name: "create_note",
arguments: { title: "Rwanda plan", content: "Next steps..." },
});
await client.close();
# pip install "mcp[cli]"
import asyncio
from mcp import ClientSession
from mcp.client.streamable_http import streamablehttp_client
MCP_URL = "https://<your-bonketiba-domain>/mcp"
async def main() -> None:
# streamablehttp_client walks OAuth 2.1 discovery at
# /.well-known/oauth-protected-resource, registers a client
# dynamically and prompts the user for consent on first run.
async with streamablehttp_client(MCP_URL) as (read, write, _):
async with ClientSession(read, write) as session:
await session.initialize()
# Discover the 10 tools (scopes: search|history|sources|notes)
tools = await session.list_tools()
print([t.name for t in tools.tools])
# Run an AI search (scope: search)
result = await session.call_tool(
"run_ai_search",
{"query": "How to open a company in Rwanda?"},
)
print(result.structuredContent)
# List saved sources (scope: sources)
await session.call_tool("list_saved_sources", {"limit": 20})
# Create a note (scope: notes)
await session.call_tool(
"create_note",
{"title": "Rwanda plan", "content": "Next steps..."},
)
asyncio.run(main())
# 1) OAuth discovery
curl https://<your-bonketiba-domain>/.well-known/oauth-protected-resource
# 2) After completing the OAuth flow you have an access token.
# Call tools with a JSON-RPC POST to /mcp.
curl -X POST https://<your-bonketiba-domain>/mcp \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "run_ai_search",
"arguments": { "query": "How to open a company in Rwanda?" }
}
}'
Requests to /mcp MUST send Accept: application/json, text/event-stream; the server may respond with either a JSON body or a single SSE frame.