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 URL | https://fencingoffice.com/api/mcp |
|---|---|
| Transport | Streamable HTTP, stateless. Every request is a POST; replies are plain JSON (no event stream). |
| Protocol versions | 2025-06-18, 2025-03-26 and 2024-11-05 |
| Sign-in | OAuth 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
- Open Settings, then Connectors, and choose Add custom connector.
- Name it Fencing Office and paste the server URL.
- Choose Connect, sign in, and pick your club.
ChatGPT
- 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.
- Choose Create, name it Fencing Office, paste the server URL and pick OAuth.
- Sign in with your Fencing Office admin account and pick your club.
- In a new chat, open the + menu, choose Developer mode and turn on Fencing Office.
Cursor
- Choose Add to Cursor. Cursor asks before it adds anything.
- Cursor opens a sign-in page. Sign in and pick your club.
{
"mcpServers": {
"fencing-office": {
"type": "http",
"url": "https://fencingoffice.com/api/mcp"
}
}
}VS Code
- Choose Add to VS Code, then Install.
- Start the server when Copilot asks, sign in and pick your club.
Claude Code
- Run this in a terminal.
- In Claude Code, type /mcp, pick fencing-office and sign in.
claude mcp add --transport http fencing-office https://fencingoffice.com/api/mcpGemini CLI
- Run this in a terminal.
- Start Gemini CLI and sign in when it opens the browser.
gemini mcp add --transport http fencing-office https://fencingoffice.com/api/mcpCodex
- Run this in a terminal, then codex mcp login fencing-office to sign in.
codex mcp add fencing-office --url https://fencingoffice.com/api/mcpAnother app
- Most apps take the server URL and sign you in. If yours asks for a config, use this.
- 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
| Method | Does |
|---|---|
initialize | Agrees on a protocol version and returns the server’s name, capabilities and instructions. |
ping | Returns an empty result. |
tools/list | Every tool this connection may use, with JSON Schemas and annotations. One page; there is no cursor. |
tools/call | Runs a tool by name (or one of the first-version names). |
resources/list, resources/read | The confirm card, for connections that may make changes. Empty otherwise. |
resources/templates/list, prompts/list | Always 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:
| Risk | readOnlyHint | destructiveHint | idempotentHint | openWorldHint |
|---|---|---|---|---|
| Lookups | true | false | ||
| Changes club data | false | false | true | false |
| Emails or messages people | false | false | false | true |
| Deletes or cancels | false | true | false | true |
| Money, minors’ data | false | true | false | false |
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:
| Code | When |
|---|---|
-32700 | The body isn’t JSON (sent with HTTP 400). |
-32600 | It isn’t a JSON-RPC 2.0 request. |
-32601 | An unknown method. |
-32602 | An unknown tool, or arguments that aren’t an object. |
-32002 | An 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).