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.
-
Take your project's connect URL. It names the project, because a project is the unit of isolation. Replace
{projectId}with theproject_idthatGET /v1/whoamireturns for your credential (the Connect your agents card on Home shows it too).https://fabric.wexa.ai/mcp/{projectId} -
Add it to your client as a remote server: a custom connector in Claude Desktop or ChatGPT, or a
urlentry in Cursor'smcp.json. Building a client: custom clients. -
Authorize. An interactive client runs OAuth in a browser; a job sends a
fab_sk_API key as a bearer token. -
Check it. Ask "Using Wexa, tell me what my current scope and quota are." The client should call
docsand name your identifiers.
Limits
| Limit | Value |
|---|---|
| Capabilities | Tools only. resources/list and prompts/list answer -32601. |
| URL | Always carries the project id. The bare /mcp and /sse paths answer 410 Gone. |
| Credential | Works only on its own project. A credential on another project's URL is refused 403. |
| Transport | Streamable HTTP. SSE is for older clients and can silently lose replies. |
| OAuth grants | Authorization code and refresh token only, so a machine caller uses an API key. |
| Proof key | S256 only. Authorization codes last five minutes and are single use. |
| Access token | Valid one hour. |
| Refresh token | Valid thirty days. It rotates on every use. |
| API key | Shown once, stored as a hash. Minting one needs an administrator role. |
What the server exposes
How it works
- 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. - 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). - 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-infoIt 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.jsonin the repository root applies to that project and gives every developer the same connection.~/.cursor/mcp.jsonapplies 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
searchtool returningid,titleandurl, and afetchtool returning whole documents. Wexa'ssearch-codeandfetch-codereturn 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_movedor "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_abc123Approval 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=xyzExchange 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-streamgives a one-shotevent: message;application/jsonalone gives a plain JSON body. - Version negotiation echoes
2024-11-05,2025-03-26or2025-06-18, in the body and theMCP-Protocol-Versionresponse header, and falls back to2025-06-18otherwise. 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.
401means the credential is missing, wrong or revoked (WWW-Authenticatesays where to re-authorize),403a credential for a different project,410a retired path,404a gone session. - A JSON-RPC
error:-32600is a missing session header,-32601an unknown method,-32602an 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_000001Post 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
- Connect an AI client: configure Claude Desktop, Cursor or ChatGPT.
- How every tool call works: tool names on each surface, the return shape, governance and errors.
- Scope and server-bound arguments: why a client may not name its own project.