MCP
Connect a client
Configuration for MCP hosts, and the raw contract for one you write yourself. Claude Code is the client this setup was verified with. The Cursor blocks are untested, and each section says what was and was not checked.
Before you start
- Make an API key in the developer console at
https://app.gogoscreen.com/dashboard/developers. Making a key needs a verified email and a paid plan or top up time. The plaintext is shown once, when the key is made, and never again. - Grant it the scopes the work needs and no more. A key’s scopes are fixed when it is made; widening them means issuing a new key.
- Choose a transport. The hosted server is the one to reach for: it needs a host that can send an HTTP header. The stdio bridge is not published as a package or a download, so it is only an option if GogoScreen has given you a copy of it.
A key is a bearer credential. Anyone holding it can spend the account’s seconds. Keep it out of files you commit, and revoke it in the console the moment you think it has leaked.
Claude Desktop
Claude Desktop’s configuration file launches local commands, which is what the stdio bridge is. It needs a copy of the bridge that GogoScreen has given you; see the note below. The file is at:
- macOS:
~/Library/Application Support/Claude/claude_desktop_config.json - Windows:
%APPDATA%\Claude\claude_desktop_config.json
Create it if it does not exist, edit it, then quit Claude Desktop completely and start it again. The file is read at startup.
{
"mcpServers": {
"gogoscreen": {
"command": "node",
"args": ["/absolute/path/to/stdio.js"],
"env": {
"GOGOSCREEN_API_KEY": "gsk_live_YOUR_KEY",
"GOGOSCREEN_API_URL": "https://api.gogoscreen.com"
}
}
}
}Claude Code
One command. It writes the configuration for you. This is the setup that was run and checked for these docs.
claude mcp add --transport http gogoscreen https://api.gogoscreen.com/mcp \
--header "Authorization: Bearer gsk_live_YOUR_KEY"The default scope is local, which is this project and your machine only. Add --scope user to make it available in every project, or --scope project to write a .mcp.json in the repository you are standing in.
A project scope entry is written into a .mcp.json inside the repository, so a key written there is a key you have given to everyone you commit it to. Keep the key in a local or user scope entry instead.
{
"mcpServers": {
"gogoscreen": {
"type": "http",
"url": "https://api.gogoscreen.com/mcp",
"headers": {
"Authorization": "Bearer gsk_live_YOUR_KEY"
}
},
"gogoscreen-stdio": {
"type": "stdio",
"command": "node",
"args": [
"/absolute/path/to/stdio.js"
],
"env": {
"GOGOSCREEN_API_KEY": "gsk_live_YOUR_KEY",
"GOGOSCREEN_API_URL": "https://api.gogoscreen.com"
}
}
}
}Check it with claude mcp list, which reports each server’s connection state. A server added at project scope has to be approved once before it connects.
A client you write yourself
Many hosts take a block of roughly the shape below, in a configuration file of their own. Check your host’s documentation for where the file lives, how it spells the transport and where the header goes. Whatever the host, it has to send the key as a header on every request.
{
"mcpServers": {
"gogoscreen": {
"type": "http",
"url": "https://api.gogoscreen.com/mcp",
"headers": { "Authorization": "Bearer gsk_live_YOUR_KEY" }
}
}
}Under that block is one POST. The hosted transport is Streamable HTTP and the server is stateless, so one POST is a complete exchange. There is no session to open, nothing to keep, and no initialize handshake required before a useful call.
| Header | Value | Why |
|---|---|---|
Authorization | Bearer gsk_live_… | Or X-API-Key: gsk_live_…. Both are accepted. A dashboard session token is not. |
Content-Type | application/json | The body is one JSON-RPC message. An array of messages (a batch) is refused with 400. |
Accept | application/json, text/event-stream | Both, because the specification allows the server to answer either way. This server always answers plain JSON and never opens an event stream. |
MCP-Protocol-Version | 2025-11-25 | Sent on every request after the handshake. Omitting it is accepted; a protocol library sends it for you. |
curl -sS https://api.gogoscreen.com/mcp \
-H "Authorization: Bearer gsk_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-H "MCP-Protocol-Version: 2025-11-25" \
-d '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"renders_list","arguments":{"limit":1}}}'{
"result": {
"content": [
{
"type": "text",
"text": "{\"data\":[{\"id\":\"1d10eb11-6bd4-4150-9ed1-b86656e71a60\",\"state\":\"queued\",\"internalStatus\":\"queued\",\"targetOrigin\":\"https://www.iana.org\",\"targetPath\":\"/\",\"hint\":\"Sign in and show the dashboard.\",\"steps\":null,\"durationSeconds\":null,\"progress\":null,\"failureCode\":null,\"watermark\":false,\"secondsReserved\":300,\"secondsCharged\":null,\"createdAt\":\"2026-09-21T17:19:40.000Z\",\"finishedAt\":null}],\"nextCursor\":\"eyJ0IjoiMjAyNi0wOS0yMVQxNzoxOTo0MC4wMDBaIiwiaSI6IjFkMTBlYjExLTZiZDQtNDE1MC05ZWQxLWI4NjY1NmU3MWE2MCJ9\"}"
}
],
"structuredContent": {
"data": [
{
"id": "1d10eb11-6bd4-4150-9ed1-b86656e71a60",
"state": "queued",
"internalStatus": "queued",
"targetOrigin": "https://www.iana.org",
"targetPath": "/",
"hint": "Sign in and show the dashboard.",
"steps": null,
"durationSeconds": null,
"progress": null,
"failureCode": null,
"watermark": false,
"secondsReserved": 300,
"secondsCharged": null,
"createdAt": "2026-09-21T17:19:40.000Z",
"finishedAt": null
}
],
"nextCursor": "eyJ0IjoiMjAyNi0wOS0yMVQxNzoxOTo0MC4wMDBaIiwiaSI6IjFkMTBlYjExLTZiZDQtNDE1MC05ZWQxLWI4NjY1NmU3MWE2MCJ9"
}
},
"jsonrpc": "2.0",
"id": 2
}Two limits. The body may be up to 1 MB, which is larger than the REST surface allows, because a tool call carries its arguments as JSON and a long marketing brief is a legitimate caller. And a request that carries an Origin header must carry an allowed one; a program that is not a browser sends none, and the absence is allowed.
With a protocol library
The program below connects, lists the tools and calls one. It was run against a live server and its output is pasted underneath, unedited.
// gogoscreen-mcp.mjs — connect, list the tools, call one.
// Needs the official MCP TypeScript SDK, @modelcontextprotocol/sdk, in this project.
// GOGOSCREEN_API_KEY=gsk_live_... node gogoscreen-mcp.mjs
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
const url = new URL(process.env.GOGOSCREEN_MCP_URL ?? "https://api.gogoscreen.com/mcp");
const client = new Client({ name: "gogoscreen-example", version: "1.0.0" });
await client.connect(
new StreamableHTTPClientTransport(url, {
requestInit: { headers: { Authorization: `Bearer ${process.env.GOGOSCREEN_API_KEY}` } },
}),
);
const { tools } = await client.listTools();
console.log(`${tools.length} tools, for example ${tools.slice(0, 3).map((t) => t.name).join(", ")}`);
const answer = await client.callTool({ name: "catalog_voices_list", arguments: {} });
if (answer.isError) {
// Every refusal is the same envelope, in the text block. Branch on the code.
const { error } = JSON.parse(answer.content[0].text);
console.error(`${error.code}: ${error.message} (requestId ${error.requestId})`);
process.exitCode = 1;
} else {
// structuredContent is the same body the HTTP operation returns.
for (const voice of answer.structuredContent.voices) {
console.log(`${voice.id.padEnd(10)} ${voice.name} — ${voice.languages.join(", ")}`);
}
}
await client.close();61 tools, for example account_me, catalog_marketing_templates_list, catalog_plans_list
sarah Sarah — en, es, pt, fr, de, it
george George — en, es, pt, fr, de, it
brian Brian — en, es, pt, fr, de, it
matilda Matilda — en, es, pt, fr, de, it
jessica Jessica — en
roger Roger — en
lily Lily — en, es, pt, fr, de, it
…A protocol library will open a GET stream after connecting, to listen for notifications this stateless server never sends. The server answers 405 and the client carries on over POST. That is normal.
The stdio bridge in detail
The stdio bridge speaks MCP on stdin and stdout and turns every tool call into an HTTPS call to https://api.gogoscreen.com/api/v1 carrying your key. It holds no authority of its own: scopes, idempotency, rate limits and the wallet are all enforced by the server on the other end. It serves the same tool list as the hosted server.
The stdio bridge is not published as a package or a download, so there is nothing to install; running it needs a copy GogoScreen has given you, started with node. Without one, use the hosted server.
| Variable | Required | What it does |
|---|---|---|
GOGOSCREEN_API_KEY | yes | Your API key. Without it the tools are still listed and every call answers unauthorized, with details.reason: "no_api_key_configured". |
GOGOSCREEN_API_URL | no | An origin or a full /api/v1 base. Both work. Defaults to https://api.gogoscreen.com. Point it at http://127.0.0.1:8000 to drive a local server. |
GOGOSCREEN_API_TIMEOUT_MS | no | How long the bridge waits for one answer. 30000 by default, five seconds past the API's own request budget. |
Diagnostics go to stderr, never to stdout, because stdout is the protocol. A driven bridge printed [gogoscreen-mcp] bridging to http://127.0.0.1:8000/api/v1 on startup, listed 61 tools, and its tool names matched the hosted server’s exactly (true).
Troubleshooting
| What you see | What it means | What to do |
|---|---|---|
401 with details.reason: "api_key_required" | The credential reached the server but is not an API key. A dashboard session token is the usual cause; the MCP server does not accept one. | Send a key that starts with gsk_, as Authorization: Bearer or X-API-Key. The REST surface accepts both kinds; this one accepts keys only. |
401 with details.reason: "no_credential" | No credential arrived at all. Usually a header the host did not forward, or a key left in a file the host is not reading. | Check the header is on the request, not only in the configuration file. Restart the host after editing its configuration. |
403 with -32000 and a sentence about Origin | The request carried a browser Origin header that is not on the server's allowlist. The fence runs before authentication, so the key was never looked at. | A host that is not a browser should send no Origin at all. If you are calling from browser code, ask for your origin to be allowed, and consider whether a page should hold a key. |
isError: true and the text MCP error -32602: Tool … not found | The tool name does not exist. Every dot in an operation id becomes an underscore, and nothing else changes. | Call tools/list and use a name from it. This is the one refusal that is not JSON, so treat a parse failure as a protocol error rather than an API error. |
402 insufficient_seconds | The seconds wallet cannot cover the work. details carries needed and available. | Do not retry. No tool buys time. The account holder adds time in the GogoScreen dashboard. |
429 rate_limited | Too many calls from this credential this minute, in the rate class named at the end of the tool description. Each rate class has its own budget; the protocol messages count as reads. | Back off. details carries class, limit and resetAt, and the HTTP response carries Retry-After and the RateLimit headers, which the bridge passes through too. |
503 with details.reason: "api_unreachable" | The stdio bridge started but could not reach the API. It answers this itself, so the tools still list. | Check GOGOSCREEN_API_URL and that the host can reach it. The bridge prints the base it resolved to on stderr at startup. |
503 with details.reason: "api_timeout" | The API accepted the connection and did not answer inside the bridge's 30 second budget. | Retry with the same idempotencyKey, so a retry cannot duplicate work that may already have started. Raise GOGOSCREEN_API_TIMEOUT_MS only on a slow link. |
| The host lists no tools at all | It never connected. For the hosted server that is usually a 401 or a 403 the host reported as a connection failure; for the bridge it is usually a wrong path or a missing Node. | Run the same call with curl, using the block above. A refusal you can read is faster than a host's connection error. |
{
"error": {
"code": "service_unavailable",
"message": "That service is temporarily unavailable.",
"details": {
"reason": "api_unreachable",
"url": "http://127.0.0.1:59999/api/v1",
"cause": "fetch failed"
},
"requestId": "d0273c9b-8f69-49b6-87ce-692264213374"
}
}