@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 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`.