@simular-ai/sai-mcp 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +164 -0
- package/SKILL.md +87 -0
- package/dist/server.js +28205 -0
- package/package.json +46 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Simular LLC
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
# @simular-ai/sai-mcp
|
|
2
|
+
|
|
3
|
+
MCP server that lets a coding agent — Claude Code, Codex, Cursor, or any MCP
|
|
4
|
+
client — delegate work to [Sai](https://sai.simular.ai), Simular's
|
|
5
|
+
computer-use agent. Sai operates a real desktop (browser, native apps, files,
|
|
6
|
+
your logged-in accounts) on a machine registered to your account; this server
|
|
7
|
+
exposes that as six tools over stdio.
|
|
8
|
+
|
|
9
|
+
Requires Node.js 22.12 or newer and a Sai API key
|
|
10
|
+
(`sapi_…`, created at <https://sai.simular.ai/settings/api-keys>).
|
|
11
|
+
|
|
12
|
+
## Install
|
|
13
|
+
|
|
14
|
+
### Claude Code
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
claude mcp add sai -e SAI_API_KEY=sapi_... -- npx -y @simular-ai/sai-mcp
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Optionally install the `sai` skill too (same as `simulang init-claude`). It
|
|
21
|
+
teaches the loop, and covers sessions where the MCP server is not loaded by
|
|
22
|
+
falling back to plain HTTP:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
npx -y @simular-ai/sai-mcp init-claude # ~/.claude/skills/sai
|
|
26
|
+
npx -y @simular-ai/sai-mcp init-claude --project # ./.claude/skills/sai
|
|
27
|
+
npx -y @simular-ai/sai-mcp init-claude --check # exit 1 when missing or stale
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
It never overwrites a `SKILL.md` you edited; `--force` does.
|
|
31
|
+
|
|
32
|
+
### Codex
|
|
33
|
+
|
|
34
|
+
`~/.codex/config.toml`:
|
|
35
|
+
|
|
36
|
+
```toml
|
|
37
|
+
[mcp_servers.sai]
|
|
38
|
+
command = "npx"
|
|
39
|
+
args = ["-y", "@simular-ai/sai-mcp"]
|
|
40
|
+
|
|
41
|
+
[mcp_servers.sai.env]
|
|
42
|
+
SAI_API_KEY = "sapi_..."
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### Cursor
|
|
46
|
+
|
|
47
|
+
`.cursor/mcp.json` (project) or `~/.cursor/mcp.json` (global):
|
|
48
|
+
|
|
49
|
+
```json
|
|
50
|
+
{
|
|
51
|
+
"mcpServers": {
|
|
52
|
+
"sai": {
|
|
53
|
+
"command": "npx",
|
|
54
|
+
"args": ["-y", "@simular-ai/sai-mcp"],
|
|
55
|
+
"env": { "SAI_API_KEY": "sapi_..." }
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
The same JSON block works for any client that speaks stdio MCP.
|
|
62
|
+
|
|
63
|
+
## Environment
|
|
64
|
+
|
|
65
|
+
| Variable | Required | Meaning |
|
|
66
|
+
| ---------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
67
|
+
| `SAI_API_KEY` | yes | A Sai API key (`sapi_…`) or a Firebase ID token. Sent as `Authorization: Bearer`. |
|
|
68
|
+
| `SAI_API_URL` | no | Cloud API base URL. Default: production (`https://simular-cloud-api-149671192594.us-central1.run.app`). |
|
|
69
|
+
| `SAI_PUSH` | no | `1` enables channel push (see below). Only with `--dangerously-load-development-channels server:sai`; otherwise results are dropped. |
|
|
70
|
+
| `SAI_EVENTS_API` | no | `1` switches to the `wait:false` + `GET /v1/agents/events` transport once cloud-api ships it. Default off: the SSE stream is collected in-process. |
|
|
71
|
+
|
|
72
|
+
## Tools
|
|
73
|
+
|
|
74
|
+
| Tool | Input | Output |
|
|
75
|
+
| ------------------ | --------------------------------------------------- | -------------------------------------------------------------------------------------------- |
|
|
76
|
+
| `sai_machines` | — | `{ machines: [{ machineId, name, kind?, online? }], default? }` — `default` when exactly one |
|
|
77
|
+
| `sai_task_start` | `{ task, machine_id?, file_ids?, new_session? }` | `{ session_id, machine_id, queued, push }` |
|
|
78
|
+
| `sai_task_wait` | `{ session_id, timeout_s? (90, max 110), cursor? }` | `{ status, events, text, approval?, cursor, error? }` |
|
|
79
|
+
| `sai_task_approve` | `{ approval_id, decision, selections? }` | `{ ok: true }` or `{ ok: false, reason: 'link_only', approval_url }` |
|
|
80
|
+
| `sai_task_abort` | `{ session_id }` | `{ ok, aborted, reason? }` |
|
|
81
|
+
| `sai_upload` | `{ path }` | `{ file_id, name, mime, size }` |
|
|
82
|
+
|
|
83
|
+
`decision` is `approve` (this one request), `approve_for_task` (trust similar
|
|
84
|
+
requests for the rest of the task) or `deny`. `status` is `running`,
|
|
85
|
+
`needs_approval`, `idle` or `error`.
|
|
86
|
+
|
|
87
|
+
There is no screenshot tool: Sai sees the screen itself and reports back in
|
|
88
|
+
text.
|
|
89
|
+
|
|
90
|
+
## The loop
|
|
91
|
+
|
|
92
|
+
The server's `instructions` teach the model this loop; it is what you will see
|
|
93
|
+
it do:
|
|
94
|
+
|
|
95
|
+
1. `sai_machines` once — skip when the machine id is already known.
|
|
96
|
+
2. `sai_task_start { task }` → `session_id`. The task keeps running on the
|
|
97
|
+
machine.
|
|
98
|
+
3. `sai_task_wait { session_id }` and repeat while `status` is `running`,
|
|
99
|
+
passing back the `cursor` each time (each call blocks up to `timeout_s`,
|
|
100
|
+
under Claude Code's two-minute tool limit).
|
|
101
|
+
- `needs_approval`: read `approval`. If `isLinkOnly` is false, decide with
|
|
102
|
+
`sai_task_approve`. If it is true (OAuth consent, credentials, phone
|
|
103
|
+
verification) the model hands `approval.approvalUrl` to you and keeps
|
|
104
|
+
waiting.
|
|
105
|
+
- `idle`: `text` is Sai's answer.
|
|
106
|
+
- `error`: `error` says what went wrong.
|
|
107
|
+
4. `sai_task_abort` to stop a task you no longer want.
|
|
108
|
+
|
|
109
|
+
One conversation per machine persists on Sai's `api` channel across calls, so
|
|
110
|
+
follow-up tasks can refer to earlier ones. `new_session: true` starts clean.
|
|
111
|
+
|
|
112
|
+
Files: `sai_upload` a local file first, then pass its `file_id` in
|
|
113
|
+
`sai_task_start.file_ids` (uploads are remembered for the lifetime of the
|
|
114
|
+
server process).
|
|
115
|
+
|
|
116
|
+
## Channels (Claude Code, research preview)
|
|
117
|
+
|
|
118
|
+
Claude Code [channels](https://code.claude.com/docs/en/channels) let a server
|
|
119
|
+
push events into the session instead of being polled. This server always
|
|
120
|
+
declares the `claude/channel` capability; with `SAI_PUSH=1` it also acts on it:
|
|
121
|
+
`sai_task_start` returns `push: true`, the model stops calling
|
|
122
|
+
`sai_task_wait`, and the server delivers `needs_approval`, `idle` and `error`
|
|
123
|
+
as `<channel source="sai" session_id=… event=… approval_id=… approval_url=…>`
|
|
124
|
+
notifications while Claude carries on with other work.
|
|
125
|
+
|
|
126
|
+
Constraints of the preview:
|
|
127
|
+
|
|
128
|
+
- Only Claude Code supports channels; Codex and Cursor poll (the default).
|
|
129
|
+
- Channels are allow-listed. Add the server with push on and load it as a
|
|
130
|
+
development channel, which skips the allowlist after a confirmation prompt
|
|
131
|
+
(the organization `channelsEnabled` policy still applies):
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
claude mcp add sai -e SAI_API_KEY=sapi_... -e SAI_PUSH=1 -- npx -y @simular-ai/sai-mcp
|
|
135
|
+
claude --dangerously-load-development-channels server:sai
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Never set `SAI_PUSH=1` without that flag: the model is told not to poll and
|
|
139
|
+
the pushed results are dropped.
|
|
140
|
+
|
|
141
|
+
- Anthropic auth only.
|
|
142
|
+
|
|
143
|
+
Polling is the baseline and works everywhere; push is an optimisation for
|
|
144
|
+
long tasks.
|
|
145
|
+
|
|
146
|
+
## Development
|
|
147
|
+
|
|
148
|
+
From the monorepo root:
|
|
149
|
+
|
|
150
|
+
```bash
|
|
151
|
+
npm run test -w @simular-ai/sai-mcp # vitest
|
|
152
|
+
npm run typecheck -w @simular-ai/sai-mcp
|
|
153
|
+
npm run build -w @simular-ai/sai-mcp # dist/server.js (esbuild bundle)
|
|
154
|
+
SAI_API_KEY=... node packages/sai-mcp/dist/server.js # run over stdio
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
Point at staging with `SAI_API_URL=https://staging.cloud-api.simular.cloud`.
|
|
158
|
+
|
|
159
|
+
## Releasing
|
|
160
|
+
|
|
161
|
+
Same as `sai-cli` (see `.github/workflows/publish-sai-mcp.yml`): bump `version`
|
|
162
|
+
in `package.json` and `SERVER_VERSION` in `src/server.ts`, merge, then
|
|
163
|
+
`git tag sai-mcp-vX.Y.Z && git push origin sai-mcp-vX.Y.Z`. CI publishes to npm
|
|
164
|
+
with trusted publishing (OIDC) and creates a GitHub release.
|
package/SKILL.md
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: sai
|
|
3
|
+
description: Hand a task to Sai, a computer-use agent on a real Windows or macOS computer, over plain HTTP with curl and jq, and bring back the result.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Sai over HTTP
|
|
7
|
+
|
|
8
|
+
Use this skill when the user asks to do something on a computer that this environment cannot do itself: open a desktop application, work in a browser with the user's accounts, read or produce files on their machine, or anything that needs a screen. Sai runs the task on the user's computer and returns text.
|
|
9
|
+
|
|
10
|
+
Do not use it for work that can be done locally in this repository.
|
|
11
|
+
|
|
12
|
+
If the `sai` MCP tools (`sai_task_start`, `sai_task_wait`, `sai_task_approve`) are available in this session, use them instead: they run the same loop below without shell commands. The HTTP steps here are for sessions without the MCP server.
|
|
13
|
+
|
|
14
|
+
## Prerequisites
|
|
15
|
+
|
|
16
|
+
- `SAI_API_KEY` is set in the environment. If it is not, stop and ask the user to create one at https://platform.simular.ai and export it. Never ask for the key value in chat.
|
|
17
|
+
- `SAI_API_URL` is optional. Default to `https://api.simular.ai`.
|
|
18
|
+
- `curl` and `jq` are on the path.
|
|
19
|
+
|
|
20
|
+
Every request carries `-H "Authorization: Bearer $SAI_API_KEY"`.
|
|
21
|
+
|
|
22
|
+
## The loop
|
|
23
|
+
|
|
24
|
+
Run these steps in order. Do not skip the poll.
|
|
25
|
+
|
|
26
|
+
1. **Start.** `POST /v1/agents/message` with `{"message": <task>, "wait": false}`. Read `sessionId` from the `202` body. Omit `machineId` unless the user named a computer or a previous `400` listed candidates.
|
|
27
|
+
2. **Poll.** `GET /v1/agents/events?sessionId=<id>&wait=30`, then repeat with `&since=<cursor>` from the last response. Print each `data-progress` text as a short progress line. Stop polling when `status` is not `running`.
|
|
28
|
+
3. **On `needs_approval`.** Read the top-level `approval` object in the poll response (same shape as a `data-approval-request` event's `data`) and check `approval.isLinkOnly`.
|
|
29
|
+
- `false`: decide whether the action is what the user asked for. If yes, `POST /v1/agents/approve` with `{"approvalId": ..., "response": "yes"}`. Use `"response": "task"` when the same kind of request will repeat within this task. Use `"no"` when it is outside the task. For `approvalType: "choice"`, add `"selections"`, one inner array per question, using values from `questions[].options[].value`; if the answer is not obvious from the task, ask the user with AskUserQuestion first.
|
|
30
|
+
- `true`: you cannot answer it. Call AskUserQuestion and hand the user `approval.approvalUrl` with one sentence saying what it is for (Google connect, a site password, phone verification). When they confirm they are done, go back to step 2 with the same cursor.
|
|
31
|
+
4. **On `idle`.** Report `text` to the user verbatim, then summarise if it is long.
|
|
32
|
+
5. **On `error`.** Report the `error` event's `errorText`. Do not retry automatically. If the event (or a `tool-output-error`) carries `code`, stop and do not start the task again today:
|
|
33
|
+
- `insufficient_credits`: tell the user their credit is spent. Free plan: it refreshes at `resetsAt` (UTC midnight; convert to their local time). Paid plan: top up at https://platform.simular.ai. Call `GET /v1/agents/account` and include `plan.title` and `credits.usd` in the message.
|
|
34
|
+
- `free_computer_time_exhausted`: tell the user the free plan's daily computer time is used up and the computer was shut down. It resets daily (exact local time in the Sai app); they can come back tomorrow or upgrade. No retry today.
|
|
35
|
+
|
|
36
|
+
To run a task on a specific model, add `"model": <id>` to the start body. Ids come from `GET /v1/agents/models` (`allowed: false` means not on this plan); `"auto"` is the default. The choice sticks to the session, so send it once.
|
|
37
|
+
|
|
38
|
+
A free account's first task of the day can stay in `running` for about two minutes while the computer starts. Keep polling.
|
|
39
|
+
|
|
40
|
+
## Rate limits and errors
|
|
41
|
+
|
|
42
|
+
- `429` on any call: wait 60 seconds and retry once. If it happens again, tell the user the account's hourly limit is reached (60 messages per hour, 1200 polls per hour) and stop. Never poll with `wait=0` in a loop.
|
|
43
|
+
- `400` on start with a `machines` array: several computers exist. Pick the one the user named, or ask with AskUserQuestion.
|
|
44
|
+
- `400 {"error":"unknown_model"}` or `403 {"error":"model_not_allowed_for_plan"}` on start: drop `model` or pick one from the `allowedModels` list in the reply.
|
|
45
|
+
- `403` on start: the `machineId` is not on this account. Call `GET /v1/agents/machines` and use one of the returned ids.
|
|
46
|
+
- `409` on start: the message was refused; report `error` to the user.
|
|
47
|
+
- `403` on poll: the `sessionId` is wrong or not this account's. Check the value you saved from the start response.
|
|
48
|
+
- `409 {"error":"link_only"}` on approve: treat as `isLinkOnly: true` and hand over `approvalUrl`.
|
|
49
|
+
|
|
50
|
+
## Cancelling
|
|
51
|
+
|
|
52
|
+
If the user cancels, or you decide the task should stop, call `POST /v1/agents/abort` with `{"sessionId": <id>}` using the session from the start response. Report `aborted` from the reply.
|
|
53
|
+
|
|
54
|
+
## Commands
|
|
55
|
+
|
|
56
|
+
Start a task and capture the session id:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
SESSION=$(curl -sS "${SAI_API_URL:-https://api.simular.ai}/v1/agents/message" \
|
|
60
|
+
-H "Authorization: Bearer $SAI_API_KEY" \
|
|
61
|
+
-H "Content-Type: application/json" \
|
|
62
|
+
-d "$(jq -n --arg m "Open Notepad, type hello, save it to the desktop as hello.txt" '{message:$m, wait:false}')" \
|
|
63
|
+
| tee /dev/stderr | jq -r .sessionId)
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Poll once (repeat, feeding `cursor` back as `since`):
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
curl -sS "${SAI_API_URL:-https://api.simular.ai}/v1/agents/events?sessionId=$SESSION&since=$CURSOR&wait=30" \
|
|
70
|
+
-H "Authorization: Bearer $SAI_API_KEY" \
|
|
71
|
+
| jq '{status, cursor, text, approval, progress: [.events[] | select(.type=="data-progress") | .data.text]}'
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Approve a command request:
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
curl -sS "${SAI_API_URL:-https://api.simular.ai}/v1/agents/approve" \
|
|
78
|
+
-H "Authorization: Bearer $SAI_API_KEY" \
|
|
79
|
+
-H "Content-Type: application/json" \
|
|
80
|
+
-d "$(jq -n --arg id "$APPROVAL_ID" '{approvalId:$id, response:"yes"}')"
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Build JSON bodies with `jq -n` as above so user text with quotes never breaks the request.
|
|
84
|
+
|
|
85
|
+
## Reference
|
|
86
|
+
|
|
87
|
+
Full API documentation: https://docs.simular.ai/sai-api/introduction. Endpoints used here: `POST /v1/agents/message`, `GET /v1/agents/events`, `POST /v1/agents/approve`, `POST /v1/agents/abort`, `GET /v1/agents/machines`, `GET /v1/agents/models`, `GET /v1/agents/account`.
|