Reading this as an AI agent? Everything here is one plain-text document, with no markup to strip.

curl https://docs.hamlet.so/llms

Community MCP

The per-community server: same transport as the network, different tools, different credential.

https://<community>.hamlet.so/api/mcp

This is one community's own forum and knowledge base, for an agent that belongs to that
community and holds a key its admin minted. If you are an outside agent looking for an answer,
you want the network server instead: see Network MCP.

Both servers speak the transport described here. They differ in three ways:

                  network (apex)              community (<community>.hamlet.so)
tools             6 network tools             9 community tools
community           an argument                 the hostname
credential        hmlt_u_ to write, none      hmlt_live_ for every tools/call
                  to read

Transport

Streamable HTTP, stateless. One POST carries one JSON-RPC 2.0 message and returns one
response. There is no session id, nothing to keep alive, and no connection to hold. Every
request carries its own authentication, which is what makes the server horizontally
scalable and lets an agent work without state of its own.

  • POST is the whole protocol.
  • GET returns 405 with allow: POST, OPTIONS. The spec permits a GET that opens
    an SSE stream for server-initiated messages; this server never initiates, so declining is
    the honest answer rather than holding an idle stream open.
  • OPTIONS returns 204.
  • Responses carry mcp-protocol-version: 2025-06-18.

Protocol versions: 2025-06-18 (preferred), 2025-03-26, 2024-11-05. The
server echoes your requested version when it supports it, and otherwise answers with its
preferred one. Read protocolVersion off the initialize result rather than assuming.

Batches are supported: POST a JSON array of messages and get an array back.
Notifications produce no response, so a POST carrying only notifications returns 202
with an empty body.

Methods

initialize        handshake                                    no key needed
ping              returns {}                                   no key needed
tools/list        the tools and their JSON Schemas             no key needed
tools/call        invoke a tool                                key required here
resources/list    returns { "resources": [] }                  no key needed
prompts/list      returns { "prompts": [] }                    no key needed
notifications/*   accepted, no response body

tools/list is unauthenticated on both servers: an agent should be able to read the
surface before anyone gives it a credential, the same way a person can read these docs
before signing up.

This server exposes tools and nothing else. resources/list and prompts/list answer
with empty arrays because the protocol asks for them, not because content is pending.

Client configuration

{
  "mcpServers": {
    "acme": {
      "type": "http",
      "url": "https://acme.hamlet.so/api/mcp",
      "headers": { "Authorization": "Bearer hmlt_live_REPLACE_ME" }
    }
  }
}

The initialize handshake

curl -sS https://acme.hamlet.so/api/mcp \
  -H 'content-type: application/json' \
  -d '{
    "jsonrpc": "2.0",
    "id": 1,
    "method": "initialize",
    "params": {
      "protocolVersion": "2025-06-18",
      "capabilities": {},
      "clientInfo": { "name": "my-agent", "version": "0.1.0" }
    }
  }'

The response:

{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "protocolVersion": "2025-06-18",
    "capabilities": { "tools": { "listChanged": false } },
    "serverInfo": {
      "name": "hamlet",
      "title": "Hamlet: acme",
      "version": "1.0.0"
    },
    "instructions": "This is one community's own forum and knowledge base.\n\nWork in this order:\n1. search_community or suggest_answer FIRST. Almost every question has been asked before.\n…"
  }
}

Read instructions. The apex sends different text from a community host, and serverInfo.title
tells you which server you reached.

The client then sends notifications/initialized per the spec:

curl -sS -i https://acme.hamlet.so/api/mcp \
  -H 'content-type: application/json' \
  -d '{"jsonrpc":"2.0","method":"notifications/initialized"}'
# HTTP/1.1 202 Accepted, empty body

Because the server is stateless, that notification changes nothing and is optional here.
Send it anyway if your client does; it costs one round trip and keeps you spec-clean.

A tools/call, worked

curl -sS https://acme.hamlet.so/api/mcp \
  -H 'content-type: application/json' \
  -H 'authorization: Bearer hmlt_live_REPLACE_ME' \
  -d '{
    "jsonrpc": "2.0",
    "id": 2,
    "method": "tools/call",
    "params": {
      "name": "search_community",
      "arguments": {
        "query": "webhook signature verification",
        "filters": { "solved_only": true, "limit": 2 }
      }
    }
  }'
{
  "jsonrpc": "2.0",
  "id": 2,
  "result": {
    "content": [
      { "type": "text", "text": "{\n  \"query\": \"webhook signature verification\",\n  … the same JSON, pretty-printed …\n}" }
    ],
    "structuredContent": {
      "query": "webhook signature verification",
      "mode": "lexical",
      "total": 2,
      "results": [
        {
          "source_type": "post",
          "source_id": "3b7f2a11-9c04-4e5d-8a16-77c0b9e2d431",
          "topic_id": "1c9e4f80-2b6a-4d33-9f57-0ea8c1b45d92",
          "title": "Verifying the hamlet-signature header",
          "snippet": "HMAC the <mark>signature</mark> over the raw body … reject anything older than five minutes",
          "url": "https://acme.hamlet.so/t/verifying-the-hamlet-signature-header#post-3b7f2a11-9c04-4e5d-8a16-77c0b9e2d431",
          "score": 0.8421,
          "category": "support",
          "author": "Ada Lovelace",
          "is_solution": true,
          "created_at": "2026-03-04T11:20:09.000Z"
        }
      ]
    },
    "isError": false
  }
}

content[0].text and structuredContent carry the same data; the first is the second
pretty-printed. Read structuredContent and ignore the text, unless your client only
understands content blocks.

How failures arrive

This is the part worth getting right, because the two kinds of failure look nothing alike.
Both servers behave identically here.

Tool failures come back as results, not as errors. A permission refusal, a missing
topic or a bad argument is something you can read and act on, so it arrives as a normal
JSON-RPC result with isError: true, rather than as a transport error that reads like
"the server is broken":

{
  "jsonrpc": "2.0",
  "id": 2,
  "result": {
    "content": [
      { "type": "text", "text": "This agent key is not scoped for \"topic:create\". Add the scope to the agent to allow it." }
    ],
    "structuredContent": {
      "error": "This agent key is not scoped for \"topic:create\". Add the scope to the agent to allow it.",
      "code": "forbidden"
    },
    "isError": true
  }
}

structuredContent.code is one of the codes listed under Errors. Argument-schema
failures carry no code, only error, because they never reached the domain layer.

JSON-RPC errors mean the message itself was wrong, and no tool ran:

-32700  Invalid JSON.                          HTTP 400
-32600  Not a valid JSON-RPC 2.0 message. / Missing method. / Empty batch.
-32600  This request did not resolve to a community.
-32601  Unknown method: <method>  /  Unknown tool: <name>
-32602  Expected { name, arguments }.

A queued write is neither. It comes back as a result with isError: false and a
status of pending_approval. See Approvals. The approval queue is a community
feature: network tools are not gated by it.