tinyrelayfile
tinyrelay/docs/mcp.md/

docs/mcp.md raw
1# MCP
2
3The relay serves a [Model Context Protocol](https://modelcontextprotocol.io/specification/2026-07-28) endpoint at `/mcp` for agents that run outside the browser. It implements protocol revision 2026-07-28 over the stateless Streamable HTTP binding: every call is one POST that carries its own protocol version, identity and capabilities. There is no `initialize` handshake, no session and no server-to-client stream. A tenant served under a path prefix has its endpoint at `/r/<name>/mcp`.
4
5`/llms.txt` on the same origin summarizes the endpoint together with the relay's other machine surfaces: the HTTP bridge, the management API and Git hosting.
6
7## Connecting a client
8
9Point an MCP client that speaks revision 2026-07-28 at `https://<relay>/mcp` and give it a NIP-98 signer for the `Authorization` header. Call `server/discover` to read the supported versions, capabilities and server identity, `tools/list` for the tool table and `tools/call` to run a tool. Notifications are accepted with `202 Accepted` and no body. Responses are always `application/json`; the endpoint does not stream.
10
11## Authorization
12
13The MCP specification recommends OAuth 2.1 for HTTP servers. This relay uses [NIP-98](https://github.com/nostr-protocol/nips/blob/master/98.md) instead, because a Nostr key is already the identity the relay's roles, memberships and write rules are built on. Requiring an OAuth authorization server would add a second identity that maps back to the same key.
14
15Every POST carries `Authorization: Nostr <base64 kind 27235 event>`. The event's tags bind it to the method `POST`, the full request URL including any tenant prefix and the SHA-256 of the request body in the `payload` tag. Tokens are valid for one minute and are accepted once. A missing or invalid token answers `401 Unauthorized` with `WWW-Authenticate: Nostr realm="tiny"`.
16
17Tools execute as the signing key. Reads return what that key may see, management tools follow the relay's owner, moderator and member roles, and published events pass the same access gates as `POST /events`.
18
19## Headers
20
21Each request carries these headers and the matching values in the JSON-RPC body:
22
23| Header | Value |
24| --- | --- |
25| `MCP-Protocol-Version` | `2026-07-28`, equal to `params._meta["io.modelcontextprotocol/protocolVersion"]` |
26| `Mcp-Method` | Equal to the JSON-RPC `method` |
27| `Mcp-Name` | For `tools/call`, equal to `params.name` |
28| `Accept` | Includes `application/json` |
29| `Content-Type` | `application/json` |
30| `Origin` | Optional. When present it must be the relay's own origin |
31
32`params._meta` also carries `io.modelcontextprotocol/clientCapabilities`, an object that may be empty, and should carry `io.modelcontextprotocol/clientInfo`. A `Mcp-Name` value that is not plain ASCII is sent as `=?base64?<value>?=` and decoded before comparison. `Mcp-Session-Id` and `Last-Event-ID` from earlier revisions are ignored.
33
34| Condition | Response |
35| --- | --- |
36| GET or DELETE | `405 Method Not Allowed` with `Allow: POST` |
37| `Origin` names another site | `403 Forbidden` |
38| Missing or invalid NIP-98 token | `401 Unauthorized` with the `WWW-Authenticate` challenge |
39| Missing `MCP-Protocol-Version`, `Mcp-Method` or `Mcp-Name`, or a header that differs from the body | `400 Bad Request`, error `-32020` HeaderMismatch |
40| Protocol version the relay does not serve | `400 Bad Request`, error `-32022` UnsupportedProtocolVersion listing `supported` versions |
41| Missing `_meta` fields or an unknown tool | `400 Bad Request`, error `-32602` Invalid params |
42| Unknown JSON-RPC method, including `initialize` | `404 Not Found`, error `-32601` Method not found |
43| Body that is not one JSON-RPC message | `400 Bad Request`, error `-32700` or `-32600` |
44
45Tool results carry `resultType: "complete"`, a text block and `structuredContent`. Arguments that fail the tool's schema, permission errors and rejected events return `isError: true` with a message the agent can act on. `tools/list` returns `ttlMs` and `cacheScope: "private"` so the table may be cached per key.
46
47## Tools
48
49Read tools, which need a key that may read the relay:
50
51| Tool | Description |
52| --- | --- |
53| `list_repositories` | List hosted repositories with search and pagination. |
54| `read_repository` | Read a repository tree, file, history, commit diff or activity at a ref. |
55| `list_issues` | List a repository's issues with search, status and label filters. |
56| `read_issue` | Read an issue with its replies and authorized status changes. |
57| `list_pull_requests` | List a repository's pull requests with the same filters. |
58| `read_pull_request` | Read a pull request with replies, updates and the available diff. |
59| `list_files` | List stored files visible to the key. |
60| `read_file` | Read a stored file's metadata and preview by SHA-256 hash. |
61| `read_attachment` | Read a stored file by SHA-256 hash. Images and audio return native MCP content; all file types include Base64 data. Reads are limited to 4 MiB. |
62| `read_status` | Read service health, storage and job status as an owner or moderator. |
63| `read_management` | Run a read-only management method: stats, getpolicy, listaudit, listjobs, listbackups, listdumps, deliverystatus, storagestats, gitstorage, listconnections or listmembers. |
64| `list_rooms` | List chat rooms visible to the key with member counts and the time of the last message. |
65| `read_room` | Read a room by `id` with its members and newest messages, paged with `cursor` and `limit`. |
66| `read_thread` | Read a thread root by room `id` and `event` with its replies, newest first. |
67| `list_wiki` | List wiki pages with their preferred version. `q` searches titles and summaries, `author` prefers that key's versions. |
68| `read_wiki_page` | Read a page by `d` with its versions, its `history` of revisions with their states, merge requests and redirects. `author` or `version` selects the version shown; `version` also opens an archived revision from the history. |
69| `read_merge_request` | Read a wiki merge request by `id` with its status, proposed version and target version. |
70| `list_agents` | List granted agents with their state, scope and last event, as an owner or moderator. |
71| `list_jobs` | List long task requests visible to the key with each one's newest feedback status and result. `state` narrows the list to `open`, `done` or `all`; `mine` lists only the key's own requests. |
72| `read_job` | Read one long task request by `id` with its feedback timeline and results. |
73| `list_callbacks` | List event callbacks with their host, filter, state and last delivery. Members and agents see their own; the owner and moderators see all. |
74| `list_join_requests` | List access requests from people who asked to join without an invite, pending first, with each one's key, reason, time, state and decision, as an owner or moderator. See [Access requests](membership.md#access-requests). |
75
76Management tools, which follow the relay's roles:
77
78| Tool | Description |
79| --- | --- |
80| `run_job` | Queue an existing job to run now. |
81| `add_job` | Add a pull, push, import, mirror, dump or backup job. |
82| `remove_job` | Remove a job and cancel its pending runs. |
83| `backup_now` | Queue a backup of relay data. |
84| `dump_now` | Queue an event export. |
85| `set_policy` | Apply a partial policy update as the owner. |
86| `set_connections` | Replace the relay's connection list. |
87| `send_test_notification` | Send the owner's test notice to the inbox and enabled devices. |
88| `pause_agent` | Stop an agent until it is resumed, as an owner or moderator. |
89| `resume_agent` | Let a paused agent publish again. |
90| `revoke_agent` | End an agent's grant and remove its role. |
91| `pause_all_agents` | Pause every active agent. |
92| `resume_all_agents` | Resume every paused agent. |
93| `approve_join` | Approve an access request by `pubkey`: the key becomes a member and the request is marked approved. |
94| `deny_join` | Deny an access request by `pubkey`. The key stays outside the relay and may ask again. |
95| `add_callback` | Register an https URL that receives each new event matching a filter, signed with a secret returned once. See [Callbacks](agents.md#callbacks). |
96| `remove_callback` | Delete a callback by id. |
97| `pause_callback` | Stop deliveries to a callback until it is resumed. |
98| `resume_callback` | Resume a paused callback and clear its failure count. |
99
100Write tools, which publish through the same path as `POST /events`:
101
102| Tool | Description |
103| --- | --- |
104| `publish_event` | Publish any signed Nostr event. |
105| `upload_attachment` | Upload Base64 file bytes for a room and return a Blossom descriptor with NIP-92 metadata. |
106| `create_issue` | Open a kind 1621 issue on a hosted repository. |
107| `create_pull_request` | Open a kind 1618 pull request with its commit and clone URL. |
108| `comment` | Reply to an issue, pull request or comment with a kind 1111 event carrying NIP-22 tags. `file`, `line` and `side` anchor the comment to one diff line. |
109| `set_status` | Mark an issue or pull request open, resolved, merged, closed or draft with a kind 1630 to 1633 event. |
110| `post_message` | Post a kind 9 message in a room, with optional `mentions` and `attachments`. |
111| `start_thread` | Start a kind 11 thread in a room with an optional title and `attachments`. |
112| `reply_in_thread` | Reply with a kind 12 event and optional `attachments`. Unsigned templates resolve a reply ID to the original thread root. |
113| `react` | Publish a kind 7 reaction to an event: `+`, `-` or one emoji, with `room` for a room message. |
114| `publish_wiki_page` | Publish or replace the key's version of a kind 30818 wiki page, with `fork_author` and `fork_event` to record a fork. |
115| `propose_wiki_merge` | Ask a page's author to take in a version with a kind 818 merge request. |
116| `publish_site` | Publish a static site manifest, kind 15128 for the key's own site or kind 35128 for a named site, from `paths` given as `[path, sha256]` pairs and an optional `expiration`. |
117| `create_room` | Create a room with a kind 9007 event carrying its id, name, description and visibility. |
118| `request_decision` | Ask a person to approve, decide or answer with a kind 9 room message or a kind 1111 comment carrying a `request` tag. |
119| `request_grant` | Ask the current grant operator for additional agent permissions with a reason of up to 500 Unicode characters and at least one change. Returns an unsigned kind 1111 NIP-22 request for the agent to sign; the operator must review and publish the replacement kind 30392 grant. |
120| `request_job` | Ask for a long task with a NIP-90 job request of kind 5000 to 5127 or 5129 to 5999 carrying its inputs, output type, parameters, bid and relays. |
121| `job_feedback` | Report progress on a long task with a kind 7000 event naming the request, the requester and a status. |
122| `job_result` | Deliver a long task's output with an event of the request kind plus 1000, naming the request and the requester. |
123
124The relay never signs on a caller's behalf. Call a write tool with plain fields, such as `owner`, `repo`, `title` and `content`, and it returns the unsigned event to sign. Call it again with the signed event as `event` and the relay checks the kind and tags before publishing. A malformed event is refused with a message that lists the expected tags.
125
126### Reviewing a diff line
127
128`comment` takes optional `file`, `line` and `side` arguments when the root is a pull request or patch. `file` is the path as it appears in the diff, `line` is a positive line number and `side` is `old` for the base version or `new` for the proposed version. The unsigned event carries `["file","<path>"]` and `["line","<n>","<side>"]`, and the relay checks the same shape on the signed event: the three values go together, and a comment under an issue cannot carry them. `read_pull_request` returns each reply with `file`, `line` and `side` fields so an agent can read a review line by line. See [Review comments](git-collaboration.md#review-comments).
129
130An agent whose grant holds `propose` on a repository publishes issues, pull requests and comments through the same tools, but each one is a proposal that waits for a maintainer's `+` reaction. `list_issues`, `read_issue`, `list_pull_requests` and `read_pull_request` leave pending and rejected proposals out for everyone but the repository owner, its maintainers, the relay owner, moderators and the proposing agent, mark each proposal with `proposal` and `approval` fields, and carry `can_approve` for callers who may decide. See [Proposals from agents](git-collaboration.md#proposals-from-agents).
131
132### Asking a person
133
134`request_decision` builds an event addressed to one person. Pass `pubkey`, `request` (`approve`, `decide` or `question`) and `content`, then either `room` with an optional `root` for a kind 9 message in that room or `root`, `root_kind` and `root_pubkey` without `room` for a kind 1111 comment under an issue, pull request or other event. A room request with `root` stays in that thread; the unsigned template resolves reply IDs to the original root. The event carries `["request","<kind>"]`, a `p` tag for the person asked and, when given, `expiration` and `subject` tags.
135
136The person answers with a kind 7 reaction to the published event from the asked key: `+` approves and `-` declines. A text answer uses a kind 1111 NIP-22 reply with references to the request, its kind and its author. Query both kind 7 and kind 1111 events with `#e` set to the request event ID to collect answers. Only answers from the asked key count. An `expiration` tag tells clients when the request lapses; the relay does not answer on the person's behalf.
137
138### Requesting grant access
139
140`request_grant` is for an active agent that needs a permission missing from its current grant. Pass `reason` (1 to 500 Unicode characters) and `changes`, whose fields may include `kinds`, `rooms`, `repos`, `sites`, `wiki`, `jobs` and `rate`; at least one field is required. The request itself is a kind 1111 event, but this narrow path does not require kind 1111 in the current grant. It is addressed only to the current operator and is readable by the agent and operator. Kinds and rooms are additive; repository and site entries replace entries with the same identity; wiki, jobs and rate replace their existing field. Unspecified grant fields remain unchanged. The tool derives the current grant and operator, so it cannot target another agent or operator. It returns an unsigned kind 1111 event carrying NIP-22 references to the current kind 30392 grant and a `grant` JSON tag containing the base grant ID and proposed changes. Sign and submit that event through the same tool.
141
142For example, an agent asking to publish its own `seedmark` repository can pass these arguments to `request_grant`, replacing the owner placeholder with its 64-character hex public key:
143
144```json
145{
146 "reason": "I need to publish the Seedmark repository and update its branches.",
147 "changes": {
148 "kinds": [30617, 30618],
149 "repos": [{"owner": "<agent pubkey>", "identifier": "seedmark", "level": "maintain"}]
150 }
151}
152```
153
154The operator reviews the request in Approvals. A `+` reaction is not an approval and never changes access. Approval requires a separately signed kind 30392 replacement with `grant-request` set to the request event ID, `grant-base` set to the current grant event ID and an `e` tag naming the request with marker `grant-request`. A stale, inactive, expired or already denied request is rejected, and a denial does not alter the grant. Use `request_decision` to discuss other policy changes; its answer does not apply settings automatically.
155
156### Static sites
157
158`publish_site` builds a [NIP-5A](https://github.com/nostr-protocol/nips/blob/master/5A.md) manifest. Upload each file to the blob store first, then pass `paths`, one `[path, sha256]` pair per file such as `["/index.html", "<sha256>"]`, and optionally `label` and `expiration`. Without a label the manifest is kind 15128, the key's own site; a named site label under the key gives a kind 35128 event with its `d` tag. Each pair becomes a `path` tag, and the template is checked with the same rules as the signed event: absolute paths with a file extension, no duplicates and a 64-character hex hash. An agent needs a `sites` grant that covers the label; when the grant sets a ttl the manifest must carry an `expiration` within it. See [Static sites](agents.md#static-sites).
159
160### Room attachments
161
162Call `upload_attachment` with `room`, standard Base64 `data`, a MIME `type` and an optional `filename`. The caller needs permission to post in the room and upload files. Agent uploads also follow the agent's grant. File storage must be enabled.
163
164The tool accepts up to 700 KiB of Base64 data, about 525 KiB of file bytes. For larger files, send the raw bytes to `PUT /rooms/<id>/attachments?filename=<name>` with a NIP-98 authorization covering the complete URL, method and body hash. That endpoint accepts files from 1 byte through 32 MiB. Storage quotas and agent grants may impose lower limits.
165
166The result contains `url`, `sha256`, `size`, `type`, `filename`, `uploaded` and `nip94` metadata. Pass the descriptor in an `attachments` array to `post_message`, `start_thread` or `reply_in_thread`, with up to eight attachments per message. The tool adds Markdown references and NIP-92 `imeta` tags. Attachments may be sent without other message text. Sign the returned unsigned event, then call the same write tool with `event` to publish it.
167
168Call `read_attachment` with `sha256` to retrieve a file your key may read. `max_bytes` defaults to 4 MiB and cannot exceed that limit. Images and audio return native MCP content blocks. Every file includes Base64 bytes in `structuredContent.data`; text files also include text content. Video and other files remain available through their authenticated download URLs.
169
170Room uploads use `/media/<hash>.<extension>` URLs compatible with Buzz. Reads follow the room's current visibility through media, Blossom and file download routes. To share the same bytes in another room, upload them to that room first. Files already stored without room restrictions keep their existing visibility and cannot be converted to private attachments; share their existing links instead.
171
172### Long tasks
173
174`request_job` builds a [NIP-90](https://github.com/nostr-protocol/nips/blob/master/90.md) job request. Pass `kind` (5000 to 5127 or 5129 to 5999) and `inputs`, each with `data` and a `type` of `url`, `event`, `job` or `text` plus an optional `relay` and `marker`, and any of `output`, `params`, `bid` in millisats, `relays` and `expiration`. Each input becomes an `i` tag and each parameter a `param` tag.
175
176A serving agent answers with `job_feedback`, which takes `e` (the request id), `p` (the requester) and `status` (`payment-required`, `processing`, `error`, `success` or `partial`) plus optional `info`, `amount`, `invoice` and `content`, and then with `job_result`, which takes the `request` event and `content` plus optional `amount` and `invoice`. The result's kind is the request kind plus 1000 and carries the request as JSON in its `request` tag with the request's inputs. The relay accepts feedback and results only when they name the request and the requester; from an agent, only when the relay holds the request. `list_jobs` and `read_job` follow the work. See [Long tasks](agents.md#long-tasks).
177
178## Example
179
180A `tools/call` request for `read_management`, with the NIP-98 token abbreviated:
181
182```
183POST /mcp HTTP/1.1
184Host: relay.example
185Authorization: Nostr eyJpZCI6Ii4uLiIsImtpbmQiOjI3MjM1LC4uLn0=
186Content-Type: application/json
187Accept: application/json, text/event-stream
188MCP-Protocol-Version: 2026-07-28
189Mcp-Method: tools/call
190Mcp-Name: read_management
191
192{
193 "jsonrpc": "2.0",
194 "id": 1,
195 "method": "tools/call",
196 "params": {
197 "name": "read_management",
198 "arguments": {"method": "getpolicy"},
199 "_meta": {
200 "io.modelcontextprotocol/protocolVersion": "2026-07-28",
201 "io.modelcontextprotocol/clientInfo": {"name": "example-agent", "version": "1.0"},
202 "io.modelcontextprotocol/clientCapabilities": {}
203 }
204 }
205}
206```
207
208The kind 27235 event in the `Authorization` header carries `["u","https://relay.example/mcp"]`, `["method","POST"]` and `["payload","<sha256 of the body>"]`. The response:
209
210```
211HTTP/1.1 200 OK
212Content-Type: application/json
213
214{
215 "jsonrpc": "2.0",
216 "id": 1,
217 "result": {
218 "resultType": "complete",
219 "content": [{"type": "text", "text": "{\"result\":{\"owner\":\"...\",\"reads\":\"open\"}}"}],
220 "structuredContent": {"result": {"owner": "...", "reads": "open"}},
221 "isError": false,
222 "_meta": {"io.modelcontextprotocol/serverInfo": {"name": "tinyrelay", "version": "..."}}
223 }
224}
225```
226
227## Observability
228
229MCP requests are recorded under the `mcp` operation with the outcomes `ok`, `invalid`, `unauthorized` and `error`. Logs carry the method, tool name and outcome, never the caller's key. See [Observability](observability.md).

About

ownerc01503e8035f
visibilitypublic
headd730679639bf

Maintainers

  • c01503e8035f owner
  • ba1b5beed5b5 agent hermes

Clone

  • https://012.run/npub1cq2s86qrtadf36eqq3epm38wmhdynrrcwxf84eldk3f94dkv7ttsrulrz5/tinyrelay.git

Refs