connacto
Docs

Developer reference

Technical reference for the connacto MCP server: the endpoint, the OAuth 2.1 flow that guards it, and the tool surface an agent sees.

Endpoint

One stateless MCP endpoint

connacto speaks MCP over Streamable HTTP at a single address:

endpoint
{origin}/api/mcp

The server runs in stateless mode: every request spins up a fresh server and transport, with no session handshake or SSE stream to keep alive. There is no server-assigned session id, so GET, POST, and DELETE all route to the same handler.

Auth

OAuth 2.1 with PKCE

Every call to /api/mcp requires an Authorization: Bearer <token> header. A request without one gets a 401 whose WWW-Authenticate header points at connacto's protected-resource metadata, so a compliant MCP client can discover the rest on its own.

Most clients, including Claude Desktop, Claude Code, and claude.ai connectors, run this whole flow for you the first time you add the endpoint: a browser tab opens for login and consent, then the client stores the token and reconnects on its own from then on. The steps below are what happens under the hood, useful if you are writing a client that does not already speak MCP auth.

1. Discover the endpoints

GET /.well-known/oauth-protected-resource/api/mcp returns the resource identifier and which authorization server backs it. GET /.well-known/oauth-authorization-server returns that server's endpoints (RFC 8414):

oauth-authorization-server
{
  "issuer": "{origin}",
  "authorization_endpoint": "{origin}/oauth/authorize",
  "token_endpoint": "{origin}/api/oauth/token",
  "registration_endpoint": "{origin}/api/oauth/register",
  "response_types_supported": ["code"],
  "grant_types_supported": ["authorization_code", "refresh_token"],
  "code_challenge_methods_supported": ["S256"],
  "token_endpoint_auth_methods_supported": ["none"]
}

2. Register a client

POST /api/oauth/register implements RFC 7591 dynamic client registration. connacto only issues public clients, since MCP clients have nowhere safe to keep a secret, so there is no client secret to store, only a client_id.

shell
curl {origin}/api/oauth/register \
  -H "Content-Type: application/json" \
  -d '{
    "redirect_uris": ["https://your-client.example/callback"],
    "client_name": "Your Client"
  }'

3. Send the user to authorize

Generate a PKCE code verifier and its S256 challenge, then send the user's browser to /oauth/authorize with response_type=code, client_id, redirect_uri, code_challenge, code_challenge_method=S256, and a state value. connacto prompts for login (email and password, or Google) if needed, then shows a consent screen naming your client. On approval it redirects to your redirect_uri with a code and your state.

4. Exchange the code for tokens

shell
curl {origin}/api/oauth/token \
  -d grant_type=authorization_code \
  -d client_id=$CLIENT_ID \
  -d code=$CODE \
  -d redirect_uri=https://your-client.example/callback \
  -d code_verifier=$CODE_VERIFIER

The response is an access_token and refresh_token. Authorization codes are single-use: connacto claims each code atomically, so a replayed or double-submitted code is rejected on the second attempt. When the access token expires, exchange the refresh token the same way with grant_type=refresh_token.

Reference

Core tools

These tools are always available, regardless of which modules you have. Use them to discover modules, create databases from them, and toggle or delete database instances. Once a built-in module is active, it also gets its own dedicated tools with full parameter and return docs on its module page.

list_modules

List available modules (reusable templates like diet tracking, workout tracking, house maintenance) that a new database can be created from.

Parameters

None. Call it with an empty object.

Returns

Every module you can see (public built-ins plus any custom ones you created) as a JSON array, including each one's slug, name, description, and fieldSchema.

list_databases

List this user's database instances: which modules are active (their dedicated tools are available) and which are toggled off. Returns each database's id, which set_module_active needs to toggle it. Call this first when asked what is being tracked or what is active.

Parameters

None. Call it with an empty object.

Returns

Your databases as a JSON array, each with databaseId, name, moduleSlug, moduleName, isActive, and createdAt.

create_module

Define a brand-new module (a reusable schema/template for a kind of thing the user wants to track) when nothing existing fits — e.g. the user says 'I want to track every cigar I smoke: where, who with, blend, price, rating.' Infer a sensible JSON Schema for the entry fields yourself, then call this to persist it. Follow up with create_database to instantiate it for the user.

Parameters

NameTypeRequiredNotes
slugstringYesURL-safe unique identifier, e.g. 'cigar-log'
namestringYesHuman-readable name, e.g. 'Cigar Log'
descriptionstringNo
fieldSchemaobjectYesA JSON Schema object describing the fields an entry in a database built from this module should have

Returns

The created module as JSON, including its id.

create_database

Instantiate a new personal database from a module, e.g. 'start a diet log for me'. For built-in modules (see list_modules), this is also the toggle: it activates that module's dedicated CRUD tools (e.g. diet_tracking_add_entry) for this user. If the module was previously toggled off, call set_module_active instead of creating a duplicate.

Parameters

NameTypeRequiredNotes
moduleSlugstringYesSlug of the module to instantiate
namestringYesA name for this database, e.g. 'My Diet Log'

Returns

The created (or reactivated) database as JSON, including its id. Returns an error if moduleSlug does not exist or you have hit your plan's database limit.

set_module_active

Activate or deactivate a previously-created database instance of a module. Deactivating locks that module's dedicated CRUD tools (they'll refuse to run) without deleting its data; reactivating unlocks them again.

Parameters

NameTypeRequiredNotes
databaseIdstringYesThe database instance to toggle
activebooleanYes

Returns

The updated database as JSON. Returns an error if databaseId is not yours, or if reactivating would exceed your plan's database limit.

delete_database

Permanently delete a database instance, freeing up its module for a fresh start. Must be deactivated first (set_module_active with active false) as a safety check. For a custom module, this also drops its dedicated table. For a built-in module, its historical entries are not purged (they become unreachable, not wiped) — this only removes the database instance itself so the module can be re-created from scratch via create_database.

Parameters

NameTypeRequiredNotes
databaseIdstringYesThe database instance to delete

Returns

A confirmation message. Returns an error if databaseId is not yours, or if the database is still active.

Reference

Tool dispatch at scale

A module's dedicated tools (e.g. diet_tracking_add_entry) are registered directly, by name, as long as you have few enough active modules that the tool list stays manageable for an LLM to pick from. Past that point, connacto stops registering each module's tools individually and switches to these two instead, which dispatch into the exact same per-module handlers by slug and short tool name:

describe_module

Once your active modules carry more tools than can be registered individually (there's no bound on how many modules can exist, so the tool list can't grow unbounded), each module gets a single _call tool instead of its dedicated ones, and this becomes the parameter reference for them. Call it with a moduleSlug from list_modules to see that module's available tools and their parameters, then run one through that module's _call tool.

Parameters

NameTypeRequiredNotes
moduleSlugstringYesSlug of one of your active modules, e.g. 'todo-list'

Returns

That module's tools as a JSON array, each with tool (the short name to pass to that module's _call tool), title, description, and params.

<module>_call

Registered once per active module (todo_list_call, workout_tracking_call, and so on) when dedicated per-tool registration is off. Invokes one tool on that module, dispatching to the exact same handler its dedicated tool would run. The module's full tool list is in this tool's own description; call describe_module for their input parameters.

Parameters

NameTypeRequiredNotes
toolstringYesShort tool name, e.g. 'add_entry'
inputobjectNoParameters for the tool, matching describe_module's params list for it

Returns

Whatever that module's tool normally returns.

Advanced

Building a custom connector

If your AI harness already speaks MCP, skip this section. Give it connacto's URL and let it run the OAuth flow above on its own. This is for harnesses that don't, such as a custom agent loop or an internal tool-calling framework, and need to talk to /api/mcp directly once you already hold an access token.

1. List available tools

shell
curl {origin}/api/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/list"
  }'

The response's result.tools array gives you each tool's name, description, and JSON Schema input shape. Feed that straight into your harness's tool or function definitions.

2. Call a tool

shell
curl {origin}/api/mcp \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -d '{
    "jsonrpc": "2.0",
    "id": 2,
    "method": "tools/call",
    "params": {
      "name": "diet_tracking_add_entry",
      "arguments": {
        "meal": "oatmeal",
        "calories": 350
      }
    }
  }'

The result comes back as result.content, an array of content blocks. connacto's tools return a single text block containing JSON. Parse it and hand it back to your model as the tool result.

3. Wire it into your agent loop

The general shape, independent of harness:

  • On startup, call tools/list and register each tool with your model using its name, description, and input schema as given.
  • When the model emits a tool call for one of these tools, forward it as a tools/call request with the same name and arguments.
  • Take the text out of result.content[0].text and return it to the model as the tool result. connacto always returns JSON-stringified data, so most harnesses can pass it through unmodified.
  • Check result.isError, or the JSON-RPC error field for transport-level failures, and surface it as a failed tool call rather than valid data.
  • On a 401, refresh your access token with the OAuth token endpoint before retrying.

A minimal fetch-based adapter looks like this.

adapter.ts
async function callConnacto(method, params, accessToken) {
  const res = await fetch("{origin}/api/mcp", {
    method: "POST",
    headers: {
      "Authorization": `Bearer ${accessToken}`,
      "Content-Type": "application/json",
      "Accept": "application/json, text/event-stream",
    },
    body: JSON.stringify({ jsonrpc: "2.0", id: 1, method, params }),
  });
  const { result, error } = await res.json();
  if (error) throw new Error(error.message);
  return result;
}

const { tools } = await callConnacto("tools/list", {}, accessToken);
const { content, isError } = await callConnacto(
  "tools/call",
  { name: "list_modules", arguments: {} },
  accessToken,
);

That's the whole surface area. No SDK is required, since connacto speaks plain MCP over HTTP with standard OAuth 2.1. If your harness has an existing MCP client library, most do, such as Python's mcp package or TypeScript's @modelcontextprotocol/sdk, prefer that over hand-rolling the JSON-RPC and OAuth calls above.

Ready to connect?
Point your agent at the endpoint below and start tracking.