Your own server code can call the same tools an AI app uses. You send an HTTPS POST to the Workspace369 MCP endpoint with a personal access token, and get one JSON answer back. This is the Model Context Protocol over HTTP, not a REST API: there is one URL, and the tool name goes in the request body.
Use this from a backend, a scheduled job, or a script. Do not call it from browser code.
Before you begin
- A workspace admin has turned MCP on. See Enable MCP and manage connected apps.
- You have a personal access token with the scopes your calls need. See Personal access tokens for scripts and agents.
- The token is in an environment variable on your server, here called
WORKSPACE369_TOKEN.
How the endpoint works
- URL:
https://mcp.workspace369.com/mcp. To pin requests to one workspace, usehttps://mcp.workspace369.com/w/<workspaceId>/mcp. A token for a different workspace is refused with a 403. - Method: POST only. Any other method returns 405.
- Stateless: every request stands alone. There is no session to open or close, and each answer is a single JSON body.
- Headers:
Authorization: Bearer <token>,Content-Type: application/json, andAccept: application/json, text/event-stream. The Accept header must list both types. - Body: a JSON-RPC 2.0 message, up to 1 MB.
- Token: a personal access token starts with
mcp_pat_.
List the tools
tools/list returns every tool you can call, with its description and input schema.
curl -s https://mcp.workspace369.com/mcp \
-H "Authorization: Bearer $WORKSPACE369_TOKEN" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}'
The answer has the form {"jsonrpc":"2.0","id":1,"result":{"tools":[ ... ]}}. Each tool has a name, a description, and an inputSchema that tells you which arguments it accepts. Read the schema before you call a tool. The same list is in the MCP tools reference.
Call a tool
tools/call takes the tool name and its arguments. This call searches clients:
curl -s https://mcp.workspace369.com/mcp \
-H "Authorization: Bearer $WORKSPACE369_TOKEN" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{
"jsonrpc": "2.0",
"id": 2,
"method": "tools/call",
"params": {
"name": "clients_search",
"arguments": { "query": "Acme", "limit": 10 }
}
}'
A successful answer looks like this:
{
"jsonrpc": "2.0",
"id": 2,
"result": {
"content": [{ "type": "text", "text": "Found 1 client ..." }],
"structuredContent": { "clients": [ ... ], "nextCursor": "..." }
}
}
Read result.structuredContent in code. result.content is the same information as text for a person or model to read.
Node.js
This helper works in Node 18 and later. It retries once after a rate limit and returns the structured result.
const ENDPOINT = "https://mcp.workspace369.com/mcp";
let nextId = 1;
export async function callTool(name, args = {}) {
for (let attempt = 0; attempt < 2; attempt++) {
const res = await fetch(ENDPOINT, {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.WORKSPACE369_TOKEN}`,
"Content-Type": "application/json",
Accept: "application/json, text/event-stream",
},
body: JSON.stringify({
jsonrpc: "2.0",
id: nextId++,
method: "tools/call",
params: { name, arguments: args },
}),
});
if (res.status === 429) {
const wait = Number(res.headers.get("Retry-After") ?? "5");
await new Promise((r) => setTimeout(r, wait * 1000));
continue;
}
if (res.status === 401) throw new Error("Token is invalid, revoked, or expired.");
if (!res.ok) throw new Error(`Workspace369 returned HTTP ${res.status}`);
const body = await res.json();
if (body.error) throw new Error(`JSON-RPC error ${body.error.code}: ${body.error.message}`);
const result = body.result;
if (result.isError) {
throw new Error(result.content?.[0]?.text ?? "The tool refused the call.");
}
if (result.structuredContent?.held_for_approval) {
return { held: true, ...result.structuredContent };
}
return result.structuredContent;
}
throw new Error("Rate limited twice in a row.");
}
Python
import os
import time
import requests
ENDPOINT = "https://mcp.workspace369.com/mcp"
_next_id = 0
def call_tool(name, arguments=None):
global _next_id
for _ in range(2):
_next_id += 1
res = requests.post(
ENDPOINT,
headers={
"Authorization": f"Bearer {os.environ['WORKSPACE369_TOKEN']}",
"Content-Type": "application/json",
"Accept": "application/json, text/event-stream",
},
json={
"jsonrpc": "2.0",
"id": _next_id,
"method": "tools/call",
"params": {"name": name, "arguments": arguments or {}},
},
timeout=60,
)
if res.status_code == 429:
time.sleep(int(res.headers.get("Retry-After", "5")))
continue
if res.status_code == 401:
raise RuntimeError("Token is invalid, revoked, or expired.")
res.raise_for_status()
body = res.json()
if "error" in body:
raise RuntimeError(f"JSON-RPC error: {body['error']}")
result = body["result"]
if result.get("isError"):
raise RuntimeError(result["content"][0]["text"])
structured = result.get("structuredContent") or {}
if structured.get("held_for_approval"):
return {"held": True, **structured}
return structured
raise RuntimeError("Rate limited twice in a row.")
Handle errors
There are two layers of errors. HTTP status codes describe the request. The isError flag on a result describes the tool call.
| What you see | What it means | What to do |
|---|---|---|
HTTP 401, {"error":"unauthorized"} | The token is missing, invalid, revoked, or expired. | Create a new token. Do not retry. |
HTTP 403, {"error":"forbidden_origin"} | The request came from a browser page on a website that is not allowed. | Call from server code instead. |
HTTP 403, {"error":"workspace_mismatch"} | You used a /w/<workspaceId>/mcp URL for a different workspace than the token. | Use the URL of the token's own workspace. |
| HTTP 405 | The request was not a POST. | Use POST. |
HTTP 429, {"error":"rate_limited"} | You went over the rate limit. | Wait for the number of seconds in the Retry-After header, then retry. |
HTTP 503, {"error":"service_unavailable"} | MCP is switched off for the whole service. | Retry later. |
HTTP 400 with a JSON-RPC error | The body is not valid JSON. | Fix the request. |
HTTP 406 with a JSON-RPC error | The Accept header does not list both application/json and text/event-stream. | Send both. |
HTTP 200, result.isError is true | The tool refused or failed. The reason is in result.content[0].text. | Show or log the reason. Do not retry unchanged. |
HTTP 200, held_for_approval is true | The call is waiting for a person to approve it. | See the next section. |
Typical isError reasons are "MCP is not enabled for this workspace", "Insufficient scope", a missing permission, a record that was not found, an opted-out client, and an out-of-allowance message when the workspace cannot pay for the call. A refused or failed call is not charged.
Calls held for approval
Tools that send something to a person are held until someone approves them in the Workspace369 app. A held call is not an error. The result looks like this:
{
"structuredContent": {
"held_for_approval": true,
"approval_id": "...",
"approval_url": "https://app.workspace369.com/settings/approvals/...",
"approval_expires_at": "2026-10-18T15:00:00.000Z"
}
}
Nothing has been sent. Do not call the tool again for the same request, because a second call would create a second request. Instead:
- Show the person the
approval_url, or tell them to open Approvals in Workspace369. - Poll
approvals_getwith{ "approval_id": "..." }. It is free. Itsstatusispending,executing,approved_done,declined,expired,failed, orinterrupted, andoutcomeholds the result once the call has run. - If the status is
interrupted, do not repeat the call. It may already have gone out. Ask the person to check the app.
A request expires after 1 hour. The full list of tools that are held is in How MCP access is controlled and billed.
Rate limits
One connection may make 120 calls per minute. The whole workspace may make 600 calls per minute. A call over the limit gets a 429 with Retry-After and is not counted against the next minute. If you need many records, ask for larger pages with the tool's limit and cursor arguments instead of many small calls.
Write tools accept an optional idempotency_key. Send the same key again to avoid doing the same thing twice after a network error.
Check your result
- Run the
tools/listrequest. You should see a list of tools. - Run a read-only
tools/call, such asclients_search. You should seestructuredContent. - Revoke the token and run the request again. You should get a 401.
- Open AI Connections (MCP) in Settings and find your calls in the Activity Log.