Skip to content
ProductBlogOpen console
Platform

MCP server

The workspace MCP server lets an MCP client administer one business. It speaks Streamable HTTP with JSON-RPC 2.0 request bodies.

POST https://mw.omazy.ai/api/v1/mcp

POST is the only method. GET returns 405: there is no server-initiated stream.

For connecting a client, see Connect an AI assistant. This page is the specification.

Field Value
name omazy-admin
title Omazy Workspace
capabilities tools

description, websiteUrl and icons are also returned, but only when the negotiated protocol revision defines them. Older revisions get the three fields above and nothing else, because sending fields a revision does not define is how you break strict clients.

Send your revision in initialize. The server answers with the revision it will actually use, which may be older than the one you asked for. Use the value in the response, not the one you sent.

Every method requires a token, including initialize and ping. There is no unauthenticated handshake.

This is deliberate and worth understanding if you are building a client. Answering 200 to an unauthenticated initialize tells the client that no authorization is needed here, so after it completes OAuth it has no auth requirement recorded and never attaches the token it just obtained. The connection then fails in a way that looks like the server rejected a valid key.

An unauthenticated call gets 401 with a challenge:

WWW-Authenticate: Bearer realm="OAuth",
resource_metadata="https://mw.omazy.ai/.well-known/oauth-protected-resource",
error="invalid_token", error_description="Missing or invalid access token"

That header is the entry point. A client with no prior knowledge of this server can start from the 401 alone.

Both documents sit at the origin root, per RFC 9728 and RFC 8414.

Document Path
Protected resource /.well-known/oauth-protected-resource
Authorization server /.well-known/oauth-authorization-server

The protected-resource document names the resource, the authorization server, and the scopes:

{
"resource": "https://mw.omazy.ai/api/v1/mcp",
"resource_name": "Omazy Workspace",
"authorization_servers": ["https://mw.omazy.ai"],
"bearer_methods_supported": ["header"],
"scopes_supported": ["mcp:read", "mcp:write"]
}
Purpose Endpoint
Dynamic registration (RFC 7591) /api/v1/oauth/register
Authorize /api/v1/oauth/authorize
Token /api/v1/oauth/token
Revoke (RFC 7009) /api/v1/oauth/revoke
Property Value
Grant types authorization_code, refresh_token
Response types code
PKCE S256, required
Client authentication none (public clients)
Scopes mcp:read, mcp:write

Registration is open, and the redirect_uri allowlist is frozen at registration and matched exactly. Hosted https:// callbacks are accepted, as are loopback http:// on 127.0.0.1, ::1 and localhost, and custom schemes. Public plain-http and URIs carrying a fragment are rejected.

Authorization codes are single-use and expire in 60 seconds. Refresh tokens rotate on use, and replaying a spent one revokes the entire token family including its access key, on the assumption that a replay means a copy leaked.

An issued access token is an ordinary workspace API key. The OAuth flow is a minting front-end, not a second identity system, so the auth gate, the role model and the request log are the same whether a key arrived through OAuth or was minted by hand.

Keys last 7 days by default, 90 at the most, and are stored hashed. Refresh before expiry or reconnect.

Three things bound what a call can do. All three apply.

Workspace scope. A key is bound to one workspace, decided at approval time from the approver’s real membership. A client cannot widen it by asking, and a tool that needs a workspace refuses an account-scoped key outright.

Role. Tools declare a minimum role, and the key carries the approver’s actual role. RoleAccount tools are reachable by any authenticated key, RoleMember needs member or above, RoleManager needs manager or above.

Scope allowlist. A key may carry an explicit list of tool names. When present, anything not on it is refused regardless of role. This is what implements read-only, and it is the reason read-only is not expressed by role alone: workspace.create is an account-scoped tool, so a member-role key could otherwise still reach it.

A read-only connection is issued at member role carrying this allowlist:

workspace.list workspace.get app.list
knowledge.list knowledge.search account.readiness
agent.get agent.brief agent.brief_item_versions
widget.list widget.config_get

Anything absent is unavailable, so an error in that list withdraws access rather than granting it.

26 tools. app is optional everywhere it appears and defaults to the workspace’s default app.

Tool Role Required Does
workspace.list account Businesses the account belongs to.
workspace.get member The business this key is scoped to.
workspace.create account business_name New business, default app, draft agent.
app.list member Apps in the business.
account.readiness member Onboarding readiness score.
Tool Role Required Does
agent.get member Full agent config: identity, live prompt, model config, capabilities, suggestion pills, and which layer owns the prompt.
agent.update manager Agent metadata. Refuses system_prompt on a Brief-managed agent.
agent.suggestions_set manager rules Replace the answer suggestion pills. Live immediately.

The Brief is the versioned source the system prompt is composed from. Edits are staged; nothing reaches the live agent until you publish.

Tool Role Required Does
agent.brief member Items, composed text, published text, and dirty.
agent.brief_item_set manager Create a block, or edit one by item_id.
agent.brief_item_enable manager item_id, enabled Include or exclude a block.
agent.brief_item_versions member item_id Prior versions of a block, newest first.
agent.brief_item_restore manager item_id, version Roll a block back.
agent.brief_item_delete manager item_id Delete a block and its history.
agent.brief_publish manager Compose the enabled blocks into the live prompt.
Tool Role Required Does
knowledge.list member Documents on the agent.
knowledge.search member query Retrieval over the knowledge base.
knowledge.add manager kind, title Add a document. Indexed on publish.
helpcenter.add_article manager title, body Add a help article.
catalog.add_product manager name Add a product.
Tool Role Required Does
widget.list member Deployments, with ids for the config tools.
widget.config_get member widget_id The editable config document.
widget.config_set manager widget_id, config Replace the config. Draft unless publish is true.
widget.create manager Create and publish a widget, returning the install snippet.

widget.config_set replaces the whole document. Read the current one, apply your edits, send it all back.

Tool Role Required Does
intake.propose manager website_url Read a website and propose brand, persona and FAQs.
onboard_business manager website_url Run the onboarding pass end to end.

agents.system_prompt is a derived column on any agent that has Brief items. Publishing composes the enabled blocks and overwrites it wholesale.

A direct write to the prompt therefore survives only until the next publish. The change looks applied, the agent answers differently for a while, and then it reverts with no record of what it replaced.

agent.get reports which layer owns the prompt:

{
"prompt": {
"characters": 6037,
"source": "brief",
"brief_items": 11,
"warning": "system_prompt is composed from the Brief and will be overwritten on the next publish..."
}
}

source is brief or direct. When it is brief, agent.update refuses a system_prompt write and names the tools to use instead. It also refuses when it cannot determine the answer, because an irreversible overwrite is not a safe default for an unknown. Pass force: true to override, accepting that the next publish undoes it and the Brief is not updated to match.

The supported path is agent.brief_item_set, then agent.brief_publish.

Protocol failures come back as JSON-RPC errors: -32700 parse error, -32601 unknown method, -32602 unknown tool, -32001 unauthorized.

Tool failures do not. They return a normal result carrying isError, which is what lets a model read the message and correct itself:

{
"content": [{ "type": "text", "text": "this agent's system_prompt is composed from 11 Brief items..." }],
"isError": true
}

The message is prose, meant to be read. A stable failure code is recorded against every call in the activity log, but it is not returned in the response, so there is currently nothing machine-readable to branch on. Treat isError as the signal and the text as the explanation.

Every call is recorded with its tool, outcome, failure code and duration. Arguments are never stored.

Owners and managers can read this in the console under Settings → Connected AI, alongside each connection’s last-used time and call count. A connection showing zero calls has never reached the server, which distinguishes a client that failed to connect from one that connected and did nothing.