MCP server
Connecting an assistant: the server address, sign-in, the tools, resources and prompts.
The MCP server lets an assistant work with the Affiliate Portal for a station: the REST API's operations as tools (all but the log upload and the printable documents), the week's order, changes and creatives as resources it can read and follow, and two prompts for the usual jobs. It follows the Model Context Protocol over streamable HTTP. Signing a week stays a person's act in the portal.
Server address
https://affiliates.radioworkflow.com/api/mcp
- Transport: streamable HTTP.
POSTcarries every message,GETopens a session's notification stream andDELETEends the session. - Protocol versions:
2025-06-18(the default),2025-03-26and2024-11-05. - One message per request, at most 1 MB. Batches are refused.
Two ways in
- A person signs in with OAuth. Add the server address in your assistant and connect. The first request answers 401 with the address of the sign-in details (
https://affiliates.radioworkflow.com/.well-known/oauth-protected-resource), which the assistant follows: it opens the portal, you sign in with your emailed link if you are not signed in already, and you allow the connection. The connection acts as you, with your roles. It lasts until it goes unused for 30 days, and 90 days at most, and you can end it any time under API and MCP in the portal. It can report the affidavits you could report, and it can never sign a week. - Automation uses a station key. Make a key under API and MCP in the portal with the scopes the tools below need, and send it as
Authorization: Bearer rwaff_k_.... A key never signs;sign_weekis not listed for it.
What each tool needs:
list_affiliations,get_week,get_status,get_changes,list_creatives,get_week_audio,export_order,get_clearance,list_weeks: Needs view access to the affiliation; a key needs the read scope.acknowledge_version: Needs an affiliation that is not read-only; a key needs the acknowledge scope.apply_log_matches,report_affidavits,report_show_airings: Needs the report or sign role on a live affiliation; a key needs the report scope.sign_week: Only for people with the sign role; no key can use it. It never signs anything.
Connecting your assistant
Every host that speaks MCP over HTTP can connect. Where the setting lives differs by product; these are the usual places. Enter the server address above, nothing else, unless you use a station key.
Desktop assistant apps
- Open the app's settings and find connectors, integrations or MCP servers.
- Add a remote or custom server and paste the server address.
- Connect. The app opens the portal in your browser; sign in and allow the connection, then return to the app.
- Start a new conversation and check that the station tools are listed.
Web chat apps that accept remote connectors
- In the app's settings, add a custom connector (it may be called a remote MCP server).
- Paste the server address and save.
- Connect and sign in to the portal when asked. The connector is then available in your conversations.
Code editors
Add an HTTP server to the editor's MCP settings, usually a JSON file named in its documentation. Without the headers entry the editor signs you in with OAuth; with it, the editor uses a station key from its environment.
{
"affiliates": {
"type": "http",
"url": "https://affiliates.radioworkflow.com/api/mcp",
"headers": {
"Authorization": "Bearer $AFFILIATE_API_KEY"
}
}
}Command-line agents
Add the server with the agent's command for remote servers, choosing the HTTP transport and the server address; pass the Authorization header for a station key. To check a key before you connect anything, list the tools yourself:
curl -s -X POST https://affiliates.radioworkflow.com/api/mcp \
-H "Authorization: Bearer $AFFILIATE_API_KEY" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'Tools
The tools are the REST endpoints, generated from the same catalogue: each takes the endpoint's parameters as one object and answers its JSON as structuredContent, which matches the tool's outputSchema (the response schema on the endpoint's page). A tool is listed only when the connection may use it somewhere. upload_log and get_document are not tools: a log goes through the REST API or the portal, and a printable document is a page, not data.
| Tool | Effect | Annotations | Key scope | Limits |
|---|---|---|---|---|
| list_affiliations | read | read-only, idempotent, closed world | read | portal:auth, portal:key, key:{prefix} |
| get_week | read | read-only, idempotent, closed world | read | portal:auth, portal:key, key:{prefix} |
| get_status | read | read-only, idempotent, closed world | read | portal:auth, portal:key, key:{prefix} |
| get_changes | read | read-only, idempotent, closed world | read | portal:auth, portal:key, key:{prefix} |
| list_creatives | read | read-only, idempotent, closed world | read | portal:auth, portal:key, key:{prefix} |
| get_week_audio | read | read-only, idempotent, closed world | read | portal:auth, portal:key, key:{prefix} |
| export_order | read | read-only, idempotent, closed world | read | portal:auth, portal:key, key:{prefix} |
| get_clearance | read | read-only, idempotent, closed world | read | portal:auth, portal:key, key:{prefix} |
| acknowledge_version | write | changes data, idempotent, closed world | acknowledge | portal:auth, portal:key, key:{prefix}, w:{prefix} |
| apply_log_matches | confirm | changes data, destructive, not idempotent, closed world | report | portal:auth, portal:key, key:{prefix}, w:{prefix} |
| report_affidavits | confirm | changes data, destructive, idempotent, closed world | report | portal:auth, portal:key, key:{prefix}, w:{prefix} |
| sign_week | returns a link | read-only, idempotent, closed world | people only | portal:auth, portal:key |
| list_weeks | read | read-only, idempotent, closed world | read | portal:auth, portal:key, key:{prefix} |
| report_show_airings | confirm | changes data, destructive, idempotent, closed world | report | portal:auth, portal:key, key:{prefix}, w:{prefix} |
Limits
Each tool's description ends with its limits, the same buckets as the REST API. Over a limit the call answers rate_limited with retry_after; see Rate limits.
| Bucket | Limit |
|---|---|
portal:auth | 10 failed sign-ins a minute per network address, checked before the credential is read |
portal:key | 60 requests a minute per credential, refilled at one a second |
key:{prefix} | 600 calls per 10 minutes per station key over REST and MCP together, not counted for a person's connection |
w:{prefix} | 120 recording calls a minute per credential |
Results and errors
- A success carries the answer twice, as
structuredContentand as JSON text, and_metawithcom.radioworkflow.affiliate/request-id(quote it when you contact the network) andcom.radioworkflow.affiliate/rate-limit-remaining. - A refusal is
isErrorwith one text block (what happened, what to do, and the reference page) and_meta["com.radioworkflow.affiliate/error"]withcode,message,http_status,retryable,retry_afterwhen there is one,extrasanddocs. It carries nostructuredContent. The codes are those of Errors. - Arguments are checked before anything is sent; a wrong one answers
validation_failednaming at most four fields. - To repeat a recording call safely, your client sends its request id back in the call's
_meta(not in its arguments) ascom.radioworkflow.affiliate/request-id: the repeat returns the first result instead of recording twice.
{
"isError": true,
"content": [
{
"type": "text",
"text": "The week changed since you read it. Read it again, then retry. ..."
}
],
"_meta": {
"com.radioworkflow.affiliate/request-id": "the id of this call",
"com.radioworkflow.affiliate/rate-limit-remaining": 57,
"com.radioworkflow.affiliate/error": {
"code": "epoch_changed",
"message": "The week changed since you read it. Read it again, then retry.",
"http_status": 409,
"retryable": false,
"extras": {
"epoch": 7
},
"docs": "https://.../docs/report-affidavits"
}
}
}Resources
Each affiliation's weeks are also resources, read with the same permission and the same answer as the tool beside them. {week} is a Monday (YYYY-MM-DD) or current, the broadcast week now running in the station's time zone. The list names the three current resources of up to 50 affiliations; the templates reach every other week. Contents are application/json, with lastModified from the week's latest version when there is one. No resource carries an audio link, so the creatives in the order resource leave out audioUrl; get_week_audio returns the link to the week's audio archive.
| URI template | Same answer as | What it holds |
|---|---|---|
affiliate://affiliations/{affiliation_id}/weeks/{week}/order | get_week | The week's order as get_week answers it, without audio links: every unit with its window and affidavit, the show days, the creatives, week.epoch and each rev. Reading it records that the station viewed the version, as get_week does. |
affiliate://affiliations/{affiliation_id}/weeks/{week}/changes | get_changes | Every published version of the week and what changed in each, as get_changes answers it. |
affiliate://affiliations/{affiliation_id}/weeks/{week}/creatives | list_creatives | The week's copy as list_creatives answers it with with_urls 0: titles, ISCIs, lengths, carts and whether the audio is ready, never an audio link. get_week_audio returns the link to the week's audio ZIP. |
Subscriptions
- Send
initialize. The answer'sMcp-Session-Idheader names your session; send it on the requests that follow. - Open the session's stream with
GETandAccept: text/event-stream. Most hosts do this for you. - Call
resources/subscribewith a resource URI. AcurrentURI follows the week that is current when you subscribe; the answer's_metanames that week's URI, and so do its notifications. The server reads the resource as you subscribe and compares each later check with that read. - While the stream is open, the server checks each followed week about every 60 seconds and sends
notifications/resources/updatedwith the URI of each resource that changed (a new version, or audio that arrived or went). Read the resource again to see what changed.
- A session holds at most 20 subscriptions; a credential follows at most 10 affiliations and weeks at once (the order and changes of one affiliation count once), keeps at most 5 sessions (a new one ends its least recently used) and 2 open streams.
- The server closes a stream after 280 seconds; open it again. A session unused for 30 minutes ends.
- A session lives on one server. If a request reaches another one, or the server restarts, the stream and the subscriptions answer 404: start a new session and subscribe again. Tool calls, reads and prompts never need a session.
- Subscriptions are best effort. To hear about every change, reliably and without a connected assistant, use webhooks.
Prompts
Two prompts walk an assistant through the usual jobs, naming the tools to call in order. Both end with the signing link: a person with the sign role signs the week in the portal.
| Prompt | Arguments | What it does |
|---|---|---|
file_this_weeks_affidavits | affiliation_id, week (optional) | Walks through reporting when each unit and program aired for one affiliation and week, confirming each batch with you, and ends with the link where a person with the sign role signs the week in the portal. |
what_is_still_due | affiliation_id | Lists what one affiliation still has to report, acknowledge or sign, and by when, without changing anything. |
Troubleshooting
| Answer | What it means | What to do |
|---|---|---|
| 401 | The credential is missing, expired or revoked. The WWW-Authenticate header points at https://affiliates.radioworkflow.com/.well-known/oauth-protected-resource. | Reconnect so the assistant signs you in again, or check the key under API and MCP in the portal. |
404 on GET, DELETE or a subscription | The session is not on the server that answered: it ended, the server restarted, or the request reached another server. | Start a new session with initialize and subscribe again. Tool calls never need a session. |
409 on GET | The session already has its stream open. | Use the open stream, or close it before opening another. |
| 429 | A stream cap (with Retry-After: 60) or a rate limit. A tool call over a limit answers rate_limited with retry_after. | Wait the time given, then retry. Streams are not needed for tool calls. |
| 413 | The message is over 1 MB. | Split a report into smaller batches. |
503 demo_mode | This copy of the portal runs on demo data, so MCP is off. | Connect to the live portal. |