Developer docs

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-resource

Transport: 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.

  1. Client fetches /.well-known/oauth-protected-resource.
  2. Client registers dynamically and starts an authorization flow.
  3. User approves on the Bonketiba consent screen.
  4. 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.

search
Run AI reasoning searches (run_ai_search)
history
Read and delete the caller's search history
sources
Read, create and delete bookmarked sources
notes
Read, create, update and delete notes

Rate limits & audit

  • Per-user cap: 60 MCP tool calls / minute. Excess calls return an isError with a retry hint.
  • Every call — success, error, denied, rate-limited — is written to mcp_audit_log with 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: search

Run a Bonketiba AI reasoning search. Returns a structured answer with checklist, cost, timeframe, operational steps, and cited sources with reliability scores.

Input
  • 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).
Output

structuredContent.answer: { summary, cost, timeframe, checklist[], steps[{title,detail}], sources[{name,url,stance,excerpt,reliability}], comparison }

Example call
{
  "name": "run_ai_search",
  "arguments": { "query": "How to open a SRLS company in Italy?" }
}
list_search_historyscope: history

List the signed-in user's recent AI search queries.

Input
  • limit (int 1–100, optional)Max entries (default 20).
Output

structuredContent.items: [{ id, query, model, created_at }]

Example call
{ "name": "list_search_history", "arguments": { "limit": 10 } }
delete_historyscope: historydestructive

Delete one search history entry.

Input
  • id (uuid, required)History entry id.
Output

content: 'Deleted <id>'

Example call
{ "name": "delete_history", "arguments": { "id": "…uuid…" } }
list_saved_sourcesscope: sources

List bookmarked sources.

Input
  • limit (int 1–200, optional)Max sources (default 50).
Output

structuredContent.items: [{ id, url, title, note, created_at }]

Example call
{ "name": "list_saved_sources", "arguments": {} }
save_sourcescope: sources

Bookmark a source URL in the Workspace.

Input
  • url (URL, required)Source URL.
  • title (string, optional)Display title.
  • note (string, optional)Personal note.
Output

structuredContent.source: { id, url, title, note, created_at }

Example call
{ "name": "save_source", "arguments": { "url": "https://example.com", "title": "Doc" } }
delete_sourcescope: sourcesdestructive

Remove a bookmarked source.

Input
  • id (uuid, required)Saved source id.
Output

content: 'Deleted <id>'

Example call
{ "name": "delete_source", "arguments": { "id": "…uuid…" } }
list_notesscope: notes

List Workspace notes, newest first.

Input
  • limit (int 1–200, optional)Max notes (default 50).
Output

structuredContent.items: [{ id, title, content, updated_at, created_at }]

Example call
{ "name": "list_notes", "arguments": { "limit": 20 } }
create_notescope: notes

Create a note in the Workspace.

Input
  • title (string, ≥1 char, required)Note title.
  • content (string, optional)Note body (markdown allowed).
Output

structuredContent.note: { id, title, content, created_at }

Example call
{ "name": "create_note", "arguments": { "title": "Idea", "content": "…" } }
update_notescope: notes

Update a note's title and/or content.

Input
  • id (uuid, required)Note id.
  • title (string, optional)New title.
  • content (string, optional)New body.
Output

structuredContent.note: { id, title, content, updated_at }

Example call
{ "name": "update_note", "arguments": { "id": "…", "content": "Updated" } }
delete_notescope: notesdestructive

Delete a Workspace note.

Input
  • id (uuid, required)Note id.
Output

content: 'Deleted <id>'

Example call
{ "name": "delete_note", "arguments": { "id": "…uuid…" } }

Connecting clients

ChatGPT / Claude / Codex

In the MCP connectors panel, add a server with URL https://<your-bonketiba-domain>/mcp. Complete OAuth consent when prompted.

Cursor / other IDE clients

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-health

Sandbox

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.

Node.js (TypeScript)
// 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();
Python
# 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())
Raw HTTP (JSON-RPC)
# 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.

● Offline