> Source: https://wexa.ai/docs/sdks-and-apis/mcp-clients

# 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).

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

2. Add it to your client as a remote server: a custom connector in [Claude Desktop](#claude-desktop) or [ChatGPT](#chatgpt), or a `url` entry in [Cursor](#cursor)'s `mcp.json`. Building a client: [custom clients](#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

| 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

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](/docs/get-started/authentication#create-an-api-key) 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.

```bash
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:

```json
{
  "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:

```json
{ "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](/docs/sdks-and-apis/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](#custom-clients)):

```json
{ "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-sse-transport)).

### 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:

```text
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:

```text
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:

```json
{
  "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:

```json
{
  "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](#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`:

```json
{
  "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:

```json
{
  "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:

```bash
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"]}'
```

```json
{
  "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`](/docs/context/search-code) and [`fetch-code`](/docs/context/fetch-code) on your [synced](/docs/context/code-sync) 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](#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:

```text
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:

```text
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`](/docs/context/search-code) and [`fetch-code`](/docs/context/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](#how-it-works). For errors, see [Troubleshooting](#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](/docs/governance/policy-and-approvals)).

**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:

```bash
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":{}}'
```

```text
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`:

```json
{
  "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`:

```json
{
  "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:

```bash
curl -sS https://fabric.wexa.ai/.well-known/oauth-authorization-server
```

```json
{
  "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:

```bash
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"]}'
```

```json
{
  "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:

```json
{ "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:

```python
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:

```text
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:

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

### Exchange the code

```bash
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>'
```

```json
{
  "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:

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

### Refresh, and the rotation rule

```bash
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:

```json
{ "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:

```bash
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:

```bash
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"}}}'
```

```text
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:

```bash
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:

```bash
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:

```json
{ "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:

```json
{ "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 }
```

```json
{ "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](/docs/governance/policy-and-approvals)).

### The SSE transport

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

```bash
curl -sS -N https://fabric.wexa.ai/sse/proj_abc123 \
  -H "Authorization: Bearer $FABRIC_API_KEY"
```

```text
event: endpoint
data: /messages?session=sse_000001
```

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

```text
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](/docs/get-started/connect-an-ai-client): configure Claude Desktop, Cursor or ChatGPT.
- [How every tool call works](/docs/sdks-and-apis/how-every-tool-call-works): tool names on each surface, the return shape, governance and errors.
- [Scope and server-bound arguments](/docs/get-started/core-ideas#know-what-scope-a-call-carries): why a client may not name its own project.
