On this page

SDKs and APIs

The Wexa MCP server

The Wexa MCP server is Wexa's own endpoint for the Model Context Protocol, so a client somebody else wrote, such as Claude Desktop, Cursor or ChatGPT, can call Wexa tools with no code. A call is governed exactly like the same call over REST. You need a project and a role on it.

  1. Take your project's connect URL. It names the project, because a project is the unit of isolation. Replace {projectId} with the project_id that GET /v1/whoami returns for your credential (the Connect your agents card on Home shows it too).

    https://fabric.wexa.ai/mcp/{projectId}
  2. Add it to your client as a remote server: a custom connector in Claude Desktop or ChatGPT, or a url entry in Cursor's mcp.json. Building a client: custom clients.

  3. Authorize. An interactive client runs OAuth in a browser; a job sends a fab_sk_ API key as a bearer token.

  4. Check it. Ask "Using Wexa, tell me what my current scope and quota are." The client should call docs and name your identifiers.

Limits

LimitValue
CapabilitiesTools only. resources/list and prompts/list answer -32601.
URLAlways carries the project id. The bare /mcp and /sse paths answer 410 Gone.
CredentialWorks only on its own project. A credential on another project's URL is refused 403.
TransportStreamable HTTP. SSE is for older clients and can silently lose replies.
OAuth grantsAuthorization code and refresh token only, so a machine caller uses an API key.
Proof keyS256 only. Authorization codes last five minutes and are single use.
Access tokenValid one hour.
Refresh tokenValid thirty days. It rotates on every use.
API keyShown once, stored as a hash. Minting one needs an administrator role.
What the server exposes

How it works

  1. The URL names a project: https://fabric.wexa.ai/mcp/{projectId}. A project is the unit of isolation, so the URL names what the session is about.
  2. The client authorizes. An interactive client runs OAuth in a browser; a job sends a fab_sk_ API key as a bearer token. A key is shown once and stored only as a hash (minting one needs an administrator role).
  3. The tools appear: every governed tool, on every deployment. One whose backing service is not configured is still listed and fails with <tool> is not configured (no data-service URL), (no harness URL) or (needs the harness and data-service URLs); that is about the deployment, so do not retry.

Read the URL template without a credential. To check any client, ask "Using Wexa, tell me what my current scope and quota are." It should call docs and name your identifiers; the wrong project means the wrong URL.

curl -sS https://fabric.wexa.ai/v1/connection-info

It exposes tools, and only tools

The handshake advertises one capability, with no resources or prompts key:

{
  "capabilities": { "tools": { "listChanged": false } },
  "protocolVersion": "2025-06-18",
  "serverInfo": { "name": "wexa-fabric", "version": "0.1.0" }
}

The methods for the other two are not implemented, rather than implemented empty:

{ "jsonrpc": "2.0", "id": 9, "error": { "code": -32601, "message": "method not found: resources/list" } }
{ "jsonrpc": "2.0", "id": 9, "error": { "code": -32601, "message": "method not found: prompts/list" } }

completion/complete, logging/setLevel and resources/templates/list answer the same way. The server implements only initialize, ping, tools/list, tools/call and the notifications/initialized and notifications/cancelled notifications. How every tool call works shows how each tool is named on all four surfaces; create-ontology alone has a different wire name, REST route and SDK method name.

Why the URL carries your project

A credential works only on its own project, so a token on another project's URL would serve one project's data under another's name. Wexa refuses instead, and the consent screen locks to the URL's project (see Custom clients):

{ "error": "forbidden", "error_description": "this credential belongs to a different project than the one in the URL" }

Two transports

Use Streamable HTTP, POST /mcp/{projectId}: initialize returns a session in Mcp-Session-Id that every later request sends back, GET opens an optional event stream and DELETE ends the session. HTTP with SSE is for older clients (details).

The unscoped endpoint is retired

The bare /mcp and /sse paths are permanently gone; POST, GET and DELETE /mcp and GET /sse all answer, with or without a credential:

HTTP/1.1 410 Gone
Content-Type: application/json

{"error":"endpoint_moved","error_description":"This URL no longer works. Each project now has its own MCP connect URL — open the dashboard's Connect page for this project and reconnect using that URL."}

410 rather than 404 tells a client not to retry, and the handler runs before authentication, so a stale client sees "moved", not a 401. Do not fall back.

This is not an MCP connection

An MCP connection is the opposite direction: a link Wexa holds to somebody else's MCP server, managed in the console. To give an agent a third-party tool, this page is the wrong place.

Claude Desktop

Claude Desktop

Claude Desktop reaches a remote server two ways:

  • Custom connector (prefer it): Claude runs OAuth and stores the token, so no key can leak.
  • Local bridge: a program Claude Desktop launches, configured in claude_desktop_config.json. It works whatever your plan offers (custom connectors do not exist on every plan) and suits a long-lived API key, such as a shared machine.

Add a custom connector

Open Settings (Ctrl+,, or File → Settings), choose Connectors, click Add, then Add custom connector, and paste the URL:

https://fabric.wexa.ai/mcp/{projectId}

Claude registers itself dynamically (no client identifier to create) and opens the consent screen; sign in, confirm the organization, department and project, and approve. Wexa's tools appear in the attachment menu at the bottom-left of the message box.

Use the local bridge

The config is ~/Library/Application Support/Claude/claude_desktop_config.json (macOS) or %APPDATA%\Claude\claude_desktop_config.json (Windows), opened (and created if missing) by Settings → Developer → Edit Config. The bridge is mcp-remote via npx, so you need Node.js. With an API key:

{
  "mcpServers": {
    "fabric-acme": {
      "command": "npx",
      "args": [
        "-y",
        "mcp-remote",
        "https://fabric.wexa.ai/mcp/proj_abc123",
        "--header",
        "Authorization:Bearer ${FABRIC_API_KEY}"
      ],
      "env": {
        "FABRIC_API_KEY": "fab_sk_..."
      }
    }
  }
}

Without a key, the bridge runs the same OAuth flow on first launch and caches the token under your home directory:

{
  "mcpServers": {
    "fabric-acme": {
      "command": "npx",
      "args": ["-y", "mcp-remote", "https://fabric.wexa.ai/mcp/proj_abc123"]
    }
  }
}

Quit Claude Desktop completely (on macOS, a restart is not enough while it stays in the dock) and reopen it; the connector appears under Manage connectors.

Check the Claude Desktop connector

Logs are in ~/Library/Logs/Claude or %APPDATA%\Claude\logs: mcp.log for connection failures, and mcp-server-<name>.log for a bridge. For errors, see Troubleshooting.

Cursor

Cursor

Cursor configures servers in mcp.json; the file's location decides who gets the connection:

  • .cursor/mcp.json in the repository root applies to that project and gives every developer the same connection.
  • ~/.cursor/mcp.json applies to every project you open, and keeps the configuration out of version control.

With an API key

A remote server is a url plus headers, with no command, args or type:

{
  "mcpServers": {
    "fabric": {
      "url": "https://fabric.wexa.ai/mcp/proj_abc123",
      "headers": {
        "Authorization": "Bearer ${env:FABRIC_API_KEY}"
      }
    }
  }
}

${env:NAME} reads Cursor's environment, so the file holds no secret; Cursor also resolves ${userHome} and ${workspaceFolder}.

With OAuth instead

Or add an auth block and let Cursor run the flow:

{
  "mcpServers": {
    "fabric": {
      "url": "https://fabric.wexa.ai/mcp/proj_abc123",
      "auth": {
        "CLIENT_ID": "${env:FABRIC_CLIENT_ID}",
        "CLIENT_SECRET": "${env:FABRIC_CLIENT_SECRET}",
        "scopes": ["fabric:query.read", "fabric:docs.read"]
      }
    }
  }
}

To get identifiers, register a client:

curl -sS -X POST https://fabric.wexa.ai/oauth/register \
  -H 'Content-Type: application/json' \
  -d '{"client_name":"cursor","redirect_uris":["http://127.0.0.1:9876/callback"]}'
{
  "client_id": "fab_client_Z6R3UrscnP5HLdkFA0MB4NV3",
  "client_secret": "fab_secret_4UCnz3b76ess8OjyC1qOg1qg",
  "client_name": "cursor",
  "redirect_uris": ["http://127.0.0.1:9876/callback"]
}

Wexa offers seven grants: fabric:query.read, fabric:ontology.write, fabric:docs.read, fabric:codesync.write, fabric:agent.run, fabric:orchestrate.read and fabric:orchestrate.write. A consent naming none falls back to query.read and docs.read. Writing context needs fabric:ontology.write, and running process flows needs both orchestrate grants.

Turn it on and check it

Enable Wexa under Customize in the sidebar, since a switched-off server is the commonest cause of a configuration that does nothing.

Then ask "Use Wexa to find where the retry policy is implemented, then show me that function." The agent should use search-code and fetch-code on your synced repository; an answer from the open buffer means the server is not connected.

If nothing appears, the server is disabled or the JSON is malformed; Cursor is quiet about both. For errors, see Troubleshooting.

ChatGPT

ChatGPT

ChatGPT connects to a remote server as a connector, added in Settings, and runs the authorization in a browser. Much of what stops people is on OpenAI's side, so check what your account offers.

Add the connector

Turn on developer mode, open Settings → Connectors, add a custom connector, and give it a name and the URL:

https://fabric.wexa.ai/mcp/{projectId}

That URL is all you need: Wexa publishes the discovery documents a connector reads. Sign in, confirm the organization, department and project, and approve.

If your dialog expects the older transport, use the SSE form, which is project-scoped too. Prefer /mcp when accepted, because SSE can silently lose replies:

https://fabric.wexa.ai/sse/{projectId}

What Wexa does not do here

  • Deep research connectors expect a search tool returning id, title and url, and a fetch tool returning whole documents. Wexa's search-code and fetch-code return code spans, so Wexa is not a research source; it works as an ordinary developer-mode connector.

Check the ChatGPT connector

Ask the question from How it works. For errors, see Troubleshooting.

Troubleshooting

Troubleshooting

  • 410 Gone, endpoint_moved or "This URL no longer works": the URL has no project in it. Add /{projectId}; the bare path will not come back.
  • 403, "this credential belongs to a different project than the one in the URL": the key or token and the URL name different projects. One is a copy-paste error.
  • 401, invalid_token: the key is wrong or revoked, or the OAuth token expired and could not refresh. In Claude Desktop, re-add the connector and, on the bridge, clear ~/.mcp-auth. In Cursor, ${env:FABRIC_API_KEY} may have resolved to nothing because Cursor was launched from the desktop and never saw your profile: launch it from a terminal once to confirm, then set the variable where the desktop session reads it.
  • The ChatGPT authorization window returns an error: Wexa checks you are a member of the consented project. Confirm your membership in the console.
  • Connected, but a call is refused: that is governance, not the connection, and the message names the grant or policy. A call that waits for a person shows as pending (policy decisions).
Writing your own client

Custom clients

Writing a client? Hosts below are https://fabric.wexa.ai; GET /v1/connection-info gives your issuer.

Start from the challenge

Call without a credential. The resource_metadata parameter names a project-scoped discovery document:

curl -sS -D - -X POST https://fabric.wexa.ai/mcp/proj_abc123 \
  -H 'Content-Type: application/json' \
  -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}'
HTTP/1.1 401 Unauthorized
Www-Authenticate: Bearer resource_metadata="https://fabric.wexa.ai/.well-known/oauth-protected-resource/mcp/proj_abc123"

{"error":"invalid_token","error_description":"missing bearer credential"}

The two discovery documents

The generic protected-resource document, at /.well-known/oauth-protected-resource:

{
  "resource": "https://fabric.wexa.ai",
  "authorization_servers": ["https://fabric.wexa.ai"],
  "bearer_methods_supported": ["header"],
  "scopes_supported": [
    "fabric:query.read", "fabric:ontology.write", "fabric:docs.read", "fabric:codesync.write",
    "fabric:agent.run", "fabric:orchestrate.read", "fabric:orchestrate.write"
  ]
}

The project-scoped variant, at /.well-known/oauth-protected-resource/mcp/{projectId}, differs only in resource:

{
  "resource": "https://fabric.wexa.ai/mcp/proj_abc123",
  "authorization_servers": ["https://fabric.wexa.ai"],
  "bearer_methods_supported": ["header"],
  "scopes_supported": [
    "fabric:query.read", "fabric:ontology.write", "fabric:docs.read", "fabric:codesync.write",
    "fabric:agent.run", "fabric:orchestrate.read", "fabric:orchestrate.write"
  ]
}

Use the project-scoped document, and send its resource back on the authorization request, so the consent screen locks to that project instead of the reader picking one by hand. The authorization-server document is the same on any route:

curl -sS https://fabric.wexa.ai/.well-known/oauth-authorization-server
{
  "issuer": "https://fabric.wexa.ai",
  "authorization_endpoint": "https://fabric.wexa.ai/oauth/authorize",
  "token_endpoint": "https://fabric.wexa.ai/oauth/token",
  "registration_endpoint": "https://fabric.wexa.ai/oauth/register",
  "response_types_supported": ["code"],
  "grant_types_supported": ["authorization_code", "refresh_token"],
  "code_challenge_methods_supported": ["S256"],
  "token_endpoint_auth_methods_supported": ["none", "client_secret_post", "client_secret_basic"]
}

Only the authorization-code and refresh-token grants exist, so a machine caller uses an API key. S256 is the only proof-key method; none is an accepted client authentication.

Register dynamically

A client mints its own identifiers, unauthenticated:

curl -sS -X POST https://fabric.wexa.ai/oauth/register \
  -H 'Content-Type: application/json' \
  -d '{"client_name":"my-client","redirect_uris":["http://127.0.0.1:9876/callback"]}'
{
  "client_id": "fab_client_Z6R3UrscnP5HLdkFA0MB4NV3",
  "client_secret": "fab_secret_4UCnz3b76ess8OjyC1qOg1qg",
  "client_name": "my-client",
  "redirect_uris": ["http://127.0.0.1:9876/callback"]
}

201 Created. A public client can ignore the secret and use none. Redirect URIs are matched exactly, so register every one, loopback ports included.

Proof key for code exchange

The challenge is required:

{ "error": "invalid_request", "error_description": "code_challenge (PKCE S256) is required" }

Send the unpadded base64url SHA-256 of a verifier as the challenge, and only the verifier to the token endpoint:

import base64, hashlib, secrets

verifier = base64.urlsafe_b64encode(secrets.token_bytes(32)).rstrip(b"=").decode()
challenge = base64.urlsafe_b64encode(
    hashlib.sha256(verifier.encode()).digest()
).rstrip(b"=").decode()

Authorize

Open this in the reader's browser:

https://fabric.wexa.ai/oauth/authorize
  ?response_type=code
  &client_id=fab_client_Z6R3UrscnP5HLdkFA0MB4NV3
  &redirect_uri=http%3A%2F%2F127.0.0.1%3A9876%2Fcallback
  &state=<random, checked on return>
  &code_challenge=<challenge>
  &code_challenge_method=S256
  &scope=fabric%3Aquery.read%20fabric%3Adocs.read
  &resource=https%3A%2F%2Ffabric.wexa.ai%2Fmcp%2Fproj_abc123

Approval redirects to your redirect_uri; check state, and use the code once within five minutes:

HTTP/1.1 302 Found
Location: http://127.0.0.1:9876/callback?code=fab_code_TEkG5ezViNc7FQSXSa-7vz29&state=xyz

Exchange the code

curl -sS -X POST https://fabric.wexa.ai/oauth/token \
  -H 'Content-Type: application/x-www-form-urlencoded' \
  -d 'grant_type=authorization_code' \
  -d 'code=fab_code_TEkG5ezViNc7FQSXSa-7vz29' \
  -d 'redirect_uri=http://127.0.0.1:9876/callback' \
  -d 'client_id=fab_client_Z6R3UrscnP5HLdkFA0MB4NV3' \
  -d 'code_verifier=<verifier>'
{
  "access_token": "eyJhbGciOiJIUzI1NiIsInR5...",
  "token_type": "Bearer",
  "expires_in": 3600,
  "refresh_token": "fab_rt_ojh07nmHYTPAtCQwn...",
  "scope": "fabric:query.read fabric:docs.read"
}

A mismatched verifier gets invalid_grant and burns the code, so restart the flow. Access tokens last one hour, refresh tokens thirty days:

{ "error": "invalid_grant", "error_description": "invalid or expired code" }

Refresh, and the rotation rule

curl -sS -X POST https://fabric.wexa.ai/oauth/token \
  -H 'Content-Type: application/x-www-form-urlencoded' \
  -d 'grant_type=refresh_token' \
  -d 'refresh_token=fab_rt_ojh07nmHYTPAtCQwn...' \
  -d 'client_id=fab_client_Z6R3UrscnP5HLdkFA0MB4NV3'

The response carries a new refresh token and revokes the old one:

{ "error": "invalid_grant", "error_description": "invalid refresh token" }

Store the new token before using the new access token, and never refresh from two places at once: a concurrent refresh logs the reader out.

Or use an API key

A client with no person in front of it sends a fab_sk_ key as a bearer token instead. An administrator mints it for one project:

curl -sS -X POST https://fabric.wexa.ai/v1/apikeys \
  -H "Authorization: Bearer $FABRIC_USER_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"org_id":"org_...","dept_id":"dept_...","project_id":"proj_...","name":"my-client"}'

A key inherits its minting role's grants (an owner's carries eight, including fabric:ontology.write and fabric:agent.run), so mint a read-only client from a read-only role.

Streamable HTTP, in full

POST /mcp/{projectId} with Authorization, Content-Type: application/json and an Accept header:

curl -sS -D - -X POST https://fabric.wexa.ai/mcp/proj_abc123 \
  -H "Authorization: Bearer $FABRIC_API_KEY" \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json, text/event-stream' \
  -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{
        "protocolVersion":"2025-06-18","capabilities":{},
        "clientInfo":{"name":"my-client","version":"1.0"}}}'
HTTP/1.1 200 OK
Content-Type: text/event-stream
Mcp-Protocol-Version: 2025-06-18
Mcp-Session-Id: sess_9d07f896af040e21edb36404903783ac

event: message
data: {"jsonrpc":"2.0","id":1,"result":{"capabilities":{"tools":{"listChanged":false}},"protocolVersion":"2025-06-18","serverInfo":{"name":"wexa-fabric","version":"0.1.0"}}}
  • Framing follows Accept: text/event-stream gives a one-shot event: message; application/json alone gives a plain JSON body.
  • Version negotiation echoes 2024-11-05, 2025-03-26 or 2025-06-18, in the body and the MCP-Protocol-Version response header, and falls back to 2025-06-18 otherwise. A request header of that name overrides the body.

notifications/initialized is answered 202 Accepted with an empty body. Then:

curl -sS -X POST https://fabric.wexa.ai/mcp/proj_abc123 \
  -H "Authorization: Bearer $FABRIC_API_KEY" \
  -H 'Mcp-Session-Id: sess_9d07f896af040e21edb36404903783ac' \
  -H 'Content-Type: application/json' -H 'Accept: application/json' \
  -d '{"jsonrpc":"2.0","id":2,"method":"tools/list"}'

A call:

curl -sS -X POST https://fabric.wexa.ai/mcp/proj_abc123 \
  -H "Authorization: Bearer $FABRIC_API_KEY" \
  -H 'Mcp-Session-Id: sess_9d07f896af040e21edb36404903783ac' \
  -H 'Content-Type: application/json' -H 'Accept: application/json' \
  -d '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"docs","arguments":{"topic":"governance"}}}'

The tool's answer is JSON serialized into the first content block's text; unwrap it for the same lifecycle_id and result as REST. DELETE with the session header answers 204 No Content.

Errors come in three layers

  • HTTP status: the request never reached a tool. 401 means the credential is missing, wrong or revoked (WWW-Authenticate says where to re-authorize), 403 a credential for a different project, 410 a retired path, 404 a gone session.
  • A JSON-RPC error: -32600 is a missing session header, -32601 an unknown method, -32602 an unknown tool or bad parameters:
{ "jsonrpc": "2.0", "id": 4, "error": { "code": -32602, "message": "unknown tool \"no-such-tool\"" } }
  • A result with isError: true: the tool ran and refused, and the text names the stage:
{ "content": [ { "type": "text", "text": "error (S4:validate-input): cypher rejected: query must start with a read clause (MATCH/OPTIONAL MATCH/WITH/UNWIND/RETURN/CALL), got \"CREATE\"" } ], "isError": true }
{ "content": [ { "type": "text", "text": "error (S2:resolve-scope): token missing required grant \"fabric:ontology.write\"" } ], "isError": true }

Surface these to the model so it can correct itself. A call held for approval arrives as isError: false with status: pending_approval (policy decisions).

The SSE transport

Use this only if you cannot use Streamable HTTP. Read the message endpoint from the first event:

curl -sS -N https://fabric.wexa.ai/sse/proj_abc123 \
  -H "Authorization: Bearer $FABRIC_API_KEY"
event: endpoint
data: /messages?session=sse_000001

Post each message there; it is answered 202 Accepted and the reply arrives on the stream:

event: message
data: {"jsonrpc":"2.0","id":1,"result":{"capabilities":{"tools":{"listChanged":false}},...}}

A reply is dropped, with no error, if you read too slowly. POST /messages has no project in its path, so the wrong-URL check does not apply and the session dies with the stream.

One trap in discovery

GET /v1/connection-info reports mcp.sse_url as the retired bare https://fabric.wexa.ai/sse. Build from mcp.url_template instead, substituting /sse/ for /mcp/.

What's next