---
title: "Windsurf"
description: "Connect Windsurf (now Devin Desktop) to Anlyon's MCP server so its agent calls your production APIs through governed actions, and give it rules for writing Anlyon code."
icon: "wind"
---

Windsurf is now called **Devin Desktop**, and its Cascade agent is becoming **Devin Local**. The MCP setup below works in both names of the app.

The agent can do production work, such as opening the issue, updating the record or refunding the charge, but only with your API tokens in its reach, and nothing checks a call before it goes out. Connect it to Anlyon instead. Your workspace's actions appear as tools. The agent names an action and passes input; Anlyon adds the credential at dispatch, so the agent never holds one. A risky call waits for an approver before anything is sent, [impact limits](/execution/impact-limits) cap what calls can do, and every call leaves a receipt.

This covers calls routed through Anlyon. A call the agent makes directly, with a token it holds, is not governed by Anlyon.

## Open the MCP configuration

In the agent panel, open the `...` menu at the top right and choose **Open MCP config file** in the MCPs section. The file is `mcp_config.json`:

| App | File |
| --- | --- |
| Devin Desktop, macOS and Linux | `~/.config/devin/mcp_config.json` |
| Devin Desktop, Windows | `%APPDATA%\devin\mcp_config.json` |
| Earlier Windsurf releases | `~/.codeium/windsurf/mcp_config.json` |

Create the file if it does not exist. The MCPs section lists each server, how many of its tools are enabled, and a switch to turn it on or off.

## Connect with OAuth

Add Anlyon's hosted MCP server. A remote server uses `serverUrl`:

```json
{
  "mcpServers": {
    "anlyon": {
      "serverUrl": "https://api.anlyon.com/mcp"
    }
  }
}
```

When the app asks you to sign in to `anlyon`, a browser opens. Sign in to Anlyon or create an account, choose the workspace and environment the agent acts in, and choose permissions. **Actions** is on. **Approve on your behalf** and **Switch between your workspaces** are off unless you turn them on.

## Connect with an API key

Use a key when the key's scopes should define exactly what the agent can do. Create it in the [console](https://console.anlyon.com) under **Settings → API keys** with `actions:read`, `actions:invoke` and `approvals:read` (all in the **Standard** preset). Add `approvals:write` if the agent should call `request_approval`.

```json
{
  "mcpServers": {
    "anlyon": {
      "serverUrl": "https://api.anlyon.com/mcp",
      "headers": {
        "Authorization": "Bearer ${env:ANLYON_API_KEY}"
      }
    }
  }
}
```

The app replaces `${env:NAME}` in `serverUrl`, `headers`, `command`, `args` and `env`. An unset variable becomes an empty string, which Anlyon refuses with `401`.

To test what your product's agent does for one of your users, add `"Anlyon-End-User": "user_123"` to `headers`. It works with an API key only. See [Act for one of your users](/agents/mcp-servers#act-for-one-of-your-users).

To run the server as a local process instead:

```json
{
  "mcpServers": {
    "anlyon": {
      "command": "npx",
      "args": ["-y", "@anlyonhq/mcp-server"],
      "env": { "ANLYON_API_KEY": "${env:ANLYON_API_KEY}" }
    }
  }
}
```

## Use the tools

| Tool | What it does |
| --- | --- |
| `action_<name>` | One tool per enabled action, with its own input schema. A new action appears within 30 seconds |
| `list_actions` | Lists the actions and says which need approval |
| `invoke_action` | Invokes an action by name |
| `request_approval` | Asks a person to decide something that is not an action |
| `check_approval` | Reads an approval's status once |
| `wait_for_approval` | Waits for the decision, 60 seconds by default, 120 at most. Returns the still-`pending` approval when time runs out |
| `list_pending_approvals`, `decide_approval` | Review approvals. With OAuth, shown only with **Approve on your behalf**. With a key, needs `approvals:decide` |
| `list_workspaces`, `select_workspace` | See and switch the workspace. OAuth only |

The agent can use at most 100 MCP tools at once, across every server. Each enabled action is one tool, so in a large workspace, turn off servers you do not need, or connect [one integration's MCP server](/agents/mcp-servers#one-integrations-mcp-server) such as `https://api.anlyon.com/mcp/github`.

## Call an action that needs approval

Ask the agent:

```text
Set ticket T-1042 to priority high. If it needs approval, wait for the decision and tell me what happened.
```

1. The agent calls `action_update-ticket-priority` with `{ "ticketId": "T-1042", "priority": "high" }`.
2. The action is gated, so nothing is sent. The tool answers `Awaiting human approval. Call wait_for_approval with approvalId=apr_...`.
3. The agent calls `wait_for_approval`. An approver decides in the console, in Slack or from their own MCP connection.
4. The tool returns the approval with `"status": "approved"`, who decided, and `payload.invocationId`. Anlyon has run the exact request that was reviewed. If it is still `pending`, the agent calls `wait_for_approval` again. If it is `denied` or `expired`, nothing was sent.

An outcome of `unknown` means the request may have reached the destination: do not call the action again. See [When the outcome is unknown](/execution/when-the-outcome-is-unknown).

You can never decide an approval that your own agent requested, from any app or with a key you created. A teammate decides it.

## Fix connection errors

| What you see | Why | What to do |
| --- | --- | --- |
| `401` with a key configured | `ANLYON_API_KEY` is unset in the app's environment, or the key is revoked or expired | Set the variable where the app starts, or check the key |
| `API key is missing required scope: actions:invoke` | The key lacks that scope | Add the scope to the key |
| `400 END_USER_HEADER_NOT_ALLOWED` | `Anlyon-End-User` was sent with an OAuth sign-in | Act for a user with an API key |
| `400 END_USER_REQUIRED` | The key requires an end user and no `Anlyon-End-User` was sent | Add the header |
| `"status": "connect_required"` | The end user has not connected the integration. Nothing was sent | Open `connectUrl` as that user, then ask again |
| Some actions are missing | The 100-tool limit was reached | Turn off servers you do not need, or use one integration's server |

See [Fix a refused connection or call](/agents/mcp-servers#fix-a-refused-connection-or-call) for the full list.

## Give the agent rules for Anlyon code

When the agent writes code that calls Anlyon with the SDK, give it the project's rules. Create `.windsurf/rules.md` (or add to your existing rules file):

````markdown
# Anlyon

- Production calls are Anlyon actions, invoked by name. Never call a provider API with a credential from agent code.
- Two keys: ANLYON_OPERATOR_KEY defines actions and stores secrets; ANLYON_AGENT_KEY invokes. Never use the operator key in agent code.
- To act for one of our users, build the client with `anlyon.forUser(userId)` from the signed-in session. Never let the model choose the user.
- Set an idempotency key on every invocation that must not run twice.
- `pendingApproval: true` means nothing was sent yet: wait with `approvals.waitForDecision`.
- `unknown` means the call may have happened. Reconcile with the destination; never retry blindly.
- Docs: https://docs.anlyon.com/llms.txt

## Define an action (operator, once)
```typescript
import { Client } from '@anlyonhq/sdk';

const ops = new Client({ apiKey: process.env.ANLYON_OPERATOR_KEY! });

await ops.secrets.put('HELPDESK_TOKEN', {
  value: process.env.HELPDESK_TOKEN!,
  allowedHosts: ['api.example.com'],
});

await ops.actions.create({
  name: 'update-ticket-priority',
  method: 'PATCH',
  urlTemplate: 'https://api.example.com/tickets/{{input.ticketId}}',
  headers: { Authorization: 'Bearer {{secret:HELPDESK_TOKEN}}' },
  inputSchema: {
    type: 'object',
    properties: { ticketId: { type: 'string' }, priority: { type: 'string' } },
    required: ['ticketId', 'priority'],
  },
  requiresApproval: true,
});
```

## Invoke it (agent)
```typescript
import { Client } from '@anlyonhq/sdk';

const anlyon = new Client({ apiKey: process.env.ANLYON_AGENT_KEY! });

const { data } = await anlyon.actions.invoke(
  'update-ticket-priority',
  { ticketId: 'T-1042', priority: 'high' },
  { idempotencyKey: 'ticket-T-1042-priority-high' },
);

if (data!.pendingApproval) {
  const decided = await anlyon.approvals.waitForDecision(data!.approvalId!);
  // decided.status: 'approved' | 'denied' | 'expired' | 'pending' (timed out, still decidable)
}
```

## Read the outcome
```typescript
const { data: invocation } = await anlyon.actions.invocation('inv_...');
// invocation.status: 'succeeded' | 'failed' | 'unknown' | ...
```
````

Then ask for code in plain terms, for example: "This service calls the helpdesk API with HELPDESK_TOKEN. Turn that call into an Anlyon action defined with the operator client, and replace the direct call with `actions.invoke` from the agent client."

## Related

- [MCP servers](/agents/mcp-servers): the full reference
- [Claude Code](/agents/claude-code), [Cursor](/agents/cursor) and [Codex](/agents/codex): the same server in other tools
- [TypeScript SDK](/sdks/typescript): the client the rules use
- [Actions](/execution/actions): templates, input schemas and gates
