MCP server

Connect Claude, ChatGPT, Cursor and other MCP clients, and the JSON-RPC methods the server answers.

Fencing Office is a remote MCP server, so AI apps that speak the Model Context Protocol can use every action in the API reference as a tool. It is the same API as the REST API: same keys, same rules, same confirm step for changes.

Server URLhttps://fencingoffice.com/api/mcp
TransportStreamable HTTP, stateless. Every request is a POST; replies are plain JSON (no event stream).
Protocol versions2025-06-18, 2025-03-26 and 2024-11-05
Sign-inOAuth 2.1 with PKCE and dynamic client registration, or a club key as a bearer token

Connect an AI app

Most apps only need the server URL: they find the sign-in on their own, you sign in with your Fencing Office admin account, pick the club and choose whether the app may make changes. These are the same steps a club admin sees under SettingsConnections.

Claude

  1. Open Settings, then Connectors, and choose Add custom connector.
  2. Name it Fencing Office and paste the server URL.
  3. Choose Connect, sign in, and pick your club.

Open Claude connectors

ChatGPT

  1. On chatgpt.com (Plus, Pro, Business, Enterprise or Edu; on a Business workspace an owner turns on developer mode first), open Settings, then Apps and connectors. Under Advanced, turn on Developer mode.
  2. Choose Create, name it Fencing Office, paste the server URL and pick OAuth.
  3. Sign in with your Fencing Office admin account and pick your club.
  4. In a new chat, open the + menu, choose Developer mode and turn on Fencing Office.

Open ChatGPT settings

Cursor

  1. Choose Add to Cursor. Cursor asks before it adds anything.
  2. Cursor opens a sign-in page. Sign in and pick your club.

Add to Cursor

{
  "mcpServers": {
    "fencing-office": {
      "type": "http",
      "url": "https://fencingoffice.com/api/mcp"
    }
  }
}

VS Code

  1. Choose Add to VS Code, then Install.
  2. Start the server when Copilot asks, sign in and pick your club.

Add to VS Code

Claude Code

  1. Run this in a terminal.
  2. In Claude Code, type /mcp, pick fencing-office and sign in.
claude mcp add --transport http fencing-office https://fencingoffice.com/api/mcp

Gemini CLI

  1. Run this in a terminal.
  2. Start Gemini CLI and sign in when it opens the browser.
gemini mcp add --transport http fencing-office https://fencingoffice.com/api/mcp

Codex

  1. Run this in a terminal, then codex mcp login fencing-office to sign in.
codex mcp add fencing-office --url https://fencingoffice.com/api/mcp

Another app

  1. Most apps take the server URL and sign you in. If yours asks for a config, use this.
  2. If it can’t sign in, create a key below and paste it instead.
{
  "mcpServers": {
    "fencing-office": {
      "type": "http",
      "url": "https://fencingoffice.com/api/mcp"
    }
  }
}

With a key, for apps that can’t sign in

Create a key under SettingsConnections (see API keys) and send it as a header. Claude Code:

claude mcp add --transport http fencing-office https://fencingoffice.com/api/mcp --header "Authorization: Bearer fo_live_…"

Apps that take a JSON config (Cursor and most others):

{
  "mcpServers": {
    "fencing-office": {
      "type": "http",
      "url": "https://fencingoffice.com/api/mcp",
      "headers": { "Authorization": "Bearer fo_live_…" }
    }
  }
}

What the app can see and do

  • A read-only connection lists only lookups. A connection that may make changes also lists the change tools, and each change asks first.
  • Tools the club turned off under SettingsAssistant aren’t listed.
  • Members come back with the fields in What a key can see, never phone numbers, birthdays, addresses or cards.
  • A few actions (reading a file someone uploaded in a chat with the in-app assistant, the member import, saving tables) work only in the app’s own assistant and are never listed.

Methods

MethodDoes
initializeAgrees on a protocol version and returns the server’s name, capabilities and instructions.
pingReturns an empty result.
tools/listEvery tool this connection may use, with JSON Schemas and annotations. One page; there is no cursor.
tools/callRuns a tool by name (or one of the first-version names).
resources/list, resources/readThe confirm card, for connections that may make changes. Empty otherwise.
resources/templates/list, prompts/listAlways empty.

Notifications (such as notifications/initialized) get 202 with no body. A JSON array of requests is answered as an array. GET and DELETE get 405, since there is no session or server-to-client stream; JSON replies carry an MCP-Protocol-Version header.

Calling it directly

You rarely need to, but every call is one JSON-RPC request:

curl https://fencingoffice.com/api/mcp \
  -H "Authorization: Bearer $FO_KEY" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -d '{"jsonrpc": "2.0", "id": 1, "method": "initialize", "params": {"protocolVersion": "2025-06-18", "capabilities": {}, "clientInfo": {"name": "my-script", "version": "1.0"}}}'
{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "protocolVersion": "2025-06-18",
    "capabilities": { "tools": { "listChanged": false } },
    "serverInfo": { "name": "fencing-office", "title": "Fencing Office", "version": "2.0.0" },
    "instructions": "Fencing Office: one fencing club's members, groups, classes, bookings and check-ins. Start with club_summary. …"
  }
}

List the tools:

{"jsonrpc": "2.0", "id": 2, "method": "tools/list"}
{
  "jsonrpc": "2.0",
  "id": 2,
  "result": {
    "tools": [
      {
        "name": "group_add_members",
        "title": "Add members to a group",
        "description": "Adds members to a group. …",
        "inputSchema": { "type": "object", "properties": { "group": { … }, "member_ids": { … }, "match": { … }, "confirm_token": { … } }, "required": ["group"] },
        "annotations": { "readOnlyHint": false, "destructiveHint": false, "idempotentHint": true, "openWorldHint": false },
        "_meta": { "ui": { "resourceUri": "ui://fencing-office/confirm.html" }, "ui/resourceUri": "ui://fencing-office/confirm.html" }
      }
    ]
  }
}

Run one. The result comes back twice: as JSON text in content for apps that read text, and as structuredContent for apps that read data. A file from file_get adds image or resource blocks to content.

{"jsonrpc": "2.0", "id": 3, "method": "tools/call", "params": {"name": "members_search", "arguments": {"query": "maya"}}}
{
  "jsonrpc": "2.0",
  "id": 3,
  "result": {
    "content": [{ "type": "text", "text": "{\n  \"total\": 1, …" }],
    "structuredContent": { "total": 1, "offset": 0, "nextOffset": null, "members": [ … ] },
    "isError": false
  }
}

Annotations

Each tool says how careful an app should be with it, so apps like Claude know which ones to ask about:

RiskreadOnlyHintdestructiveHintidempotentHintopenWorldHint
Lookupstruefalse
Changes club datafalsefalsetruefalse
Emails or messages peoplefalsefalsefalsetrue
Deletes or cancelsfalsetruefalsetrue
Money, minors’ datafalsetruefalsefalse

Changes and the confirm card

Every change tool works as in the REST API: the first call returns a preview and a confirm_token, and nothing happens until a second call with the same arguments and that token. The server instructions tell the app to show the preview and wait for a yes.

In apps that show MCP Apps cards (Claude and ChatGPT among them), each change tool points at the card ui://fencing-office/confirm.html (text/html;profile=mcp-app). The card shows the preview with Confirm and Cancel buttons, and the change runs only when the person presses Confirm. The card loads nothing from the network and embeds no other pages: its connect, resource and frame domain lists are all empty, in both the standard keys and ChatGPT’s older openai/widgetCSP keys, so no app asks anyone to allow an outside site.

Errors

Problems with the request itself are JSON-RPC errors:

CodeWhen
-32700The body isn’t JSON (sent with HTTP 400).
-32600It isn’t a JSON-RPC 2.0 request.
-32601An unknown method.
-32602An unknown tool, or arguments that aren’t an object.
-32002An unknown resource.

A tool that runs and fails (bad input, a refusal, a used token) returns a normal result with isError: true and a message the app can show or act on. Failures on our side say only “The tool failed. Try again, or narrow the request.”

{
  "jsonrpc": "2.0",
  "id": 4,
  "result": {
    "content": [{ "type": "text", "text": "No group called \"Adluts\". Call groups_list for the names." }],
    "isError": true
  }
}

Before any of that, the HTTP layer can answer 401 (no key, or a revoked or expired one, with WWW-Authenticate pointing to the sign-in), 403 (a browser request from another site) or 429 (over 120 requests a minute for this key; wait for Retry-After).