@soba-so/mcp 0.0.0-stage → 0.0.3

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.
Files changed (3) hide show
  1. package/README.md +73 -2
  2. package/dist/mcp.mjs +355 -0
  3. package/package.json +39 -4
package/README.md CHANGED
@@ -1,3 +1,74 @@
1
- # Temporary Holding Version
1
+ # @soba-so/mcp
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Sets Soba up from inside your coding agent.
4
+
5
+ ```sh
6
+ claude mcp add soba -- npx -y @soba-so/mcp --token mk_soba_…
7
+ ```
8
+
9
+ The token is a **management key**, minted on the Integrate page of your Soba
10
+ dashboard, which prints the whole line above with the key already in it. Then,
11
+ in Claude Code:
12
+
13
+ ```
14
+ /soba:set_up
15
+ ```
16
+
17
+ Pair it with the skill, which carries the codebase half and works before any of
18
+ this is connected:
19
+
20
+ ```sh
21
+ npx skills add https://soba.so/skill
22
+ ```
23
+
24
+ ## What it does
25
+
26
+ The step Soba's setup waits on is not dashboard work at all: it happens in your
27
+ repo. The dashboard used to hand you a prompt, you carried it to your editor
28
+ yourself, and the dashboard went blind afterwards.
29
+
30
+ This closes that loop. The agent reads the checklist, does the next outstanding
31
+ step, and reads it back — and the dashboard ticks along as it goes.
32
+
33
+ | Tool | |
34
+ |---|---|
35
+ | `soba_status` | The checklist and which steps are done |
36
+ | `soba_next_step` | The first outstanding one, with an instruction written for an agent |
37
+ | `soba_create_key` | Mints your app's secret key, once |
38
+ | `soba_set_connect_url` | Points Connect back at your app |
39
+ | `soba_create_plan` | Creates a plan from numbers **you** gave it |
40
+
41
+ ## What it deliberately does not do
42
+
43
+ **It never edits a file.** Your agent already has Read, Edit and Bash, already
44
+ has the repo open, and already shows you its diffs. A second set of file tools
45
+ tunnelled through this server would do the same job worse, somewhere you are not
46
+ looking.
47
+
48
+ **It cannot spend your money, and it cannot run as your users.** A management key
49
+ is not your `sk_soba_` key and neither authenticates as the other. It can start
50
+ one narrow kind of run — a *setup* run, on a machine you paired yourself — and
51
+ two things bound that, both enforced where they already were rather than invented
52
+ for this:
53
+
54
+ - It runs under `development`, which allows exactly `user-hardware` and
55
+ `user-subscription`. There is nothing billable in that list, so a setup run
56
+ cannot reach metered supply however it is addressed.
57
+ - It cannot choose whose machine to run on. The end user is pinned to your app's
58
+ own owner row by the gateway, not read from the request.
59
+
60
+ It also never stamps `last_seen_at`, so an agent working on your behalf cannot
61
+ make your dashboard claim your integration is live when it isn't.
62
+
63
+ **It will not price your product.** `soba_create_plan` exists; there is no
64
+ instruction telling an agent to call it. A plan is a price, an allowance, a
65
+ ceiling and a reward, and those are decisions about your business.
66
+
67
+ ## Options
68
+
69
+ | | |
70
+ |---|---|
71
+ | `--token mk_soba_…` | Your management key. `SOBA_MANAGEMENT_KEY` works too, and keeps it out of the process list. |
72
+ | `--url` | The Soba instance. Defaults to `https://soba.so`. |
73
+
74
+ No runtime dependencies. Node 22+.
package/dist/mcp.mjs ADDED
@@ -0,0 +1,355 @@
1
+ #!/usr/bin/env node
2
+ var u=class extends Error{constructor(e,r){super(r);this.status=e;this.name="ApiError"}},c=class{constructor(t,e){this.token=t;this.baseUrl=e}get(t){return this.call("GET",t)}post(t,e){return this.call("POST",t,e)}patch(t,e){return this.call("PATCH",t,e)}async call(t,e,r){let s=await fetch(`${this.baseUrl}${e}`,{method:t,headers:{authorization:`Bearer ${this.token}`,...r===void 0?{}:{"content-type":"application/json"}},body:r===void 0?void 0:JSON.stringify(r)}),o=await s.text(),i=null;try{i=o?JSON.parse(o):null}catch{throw new u(s.status,`${t} ${e} did not return json (${o.slice(0,120)})`)}if(!s.ok){let l=i?.error;throw new u(s.status,typeof l=="string"?l:`${t} ${e} failed with ${s.status}`)}return i}};var y=36e5,b=24*y,B={returnAfterMs:24*y,expiryMs:30*b,uniquenessMs:365*b};var H=10*6e4;var M=864e5,E=30,ee={abandonedAfterMs:E*M};var re=30*24*60*60*1e3;var oe=5*60*1e3;var _e=60*6e4;var T="https://soba.so/skill",ve=`npx skills add ${T}`;var w=`Soba is the LLM layer for an application whose users bring their own compute.
3
+ Requests go to Soba instead of to a model provider; Soba routes each one to the
4
+ machine or plan belonging to the user it names, meters it, and bills for it.
5
+
6
+ ## The two edits
7
+
8
+ Two edits make a Soba app work. Everything after them makes it worth having, and
9
+ an app that stops at two has nowhere for a run to land \u2014 so do these first, then
10
+ keep going.
11
+
12
+ ### 1. Point every LLM call site at Soba
13
+
14
+ baseURL https://api.soba.so/v1
15
+ apiKey process.env.SOBA_KEY
16
+
17
+ An OpenAI client already speaks this, so on most projects the base URL is the
18
+ only line that changes. Prompts, tools, streaming and error handling stay
19
+ exactly as they are.
20
+
21
+ Pass the signed-in user's id as the \`user\` field on every request.
22
+
23
+ **Resolve that id on the SERVER, from the session, never from the request
24
+ body.** It is what decides whose machine a run may reach, so a value the browser
25
+ can choose is a value any user can use to spend any other user's compute. This
26
+ is the one line in the integration that must not be guessed at: if the project's
27
+ auth is not obvious, read it until it is, or stop and ask. A wrong id here does
28
+ not throw \u2014 it silently pairs runs to the wrong person.
29
+
30
+ ### Where the key comes from
31
+
32
+ If \`.env.local\` already sets \`SOBA_KEY\`, use it. Otherwise run this from the
33
+ project root:
34
+
35
+ \`\`\`bash
36
+ npx -y @soba-so/cli login
37
+ \`\`\`
38
+
39
+ It opens the person's browser, they pick the app, and it writes \`SOBA_KEY\` (a
40
+ development key, for this machine) and \`SOBA_PROD_KEY\` (production) into
41
+ \`.env.local\`, adding that file to \`.gitignore\` if git would commit it. The key
42
+ never passes through you. Tell the person to approve in the browser before you
43
+ run it; it waits up to fifteen minutes for them, so give the command a long
44
+ timeout or run it in the background. Add \`--headless\` where no browser can
45
+ open, and pass the link it prints on to the person.
46
+
47
+ \`.env.local\` is right for Next.js, Vite and most Node servers. Where the code
48
+ that holds the key reads a different file, pass that file with \`--env-file\`
49
+ instead, from the project root, so the key lands where the server will look:
50
+
51
+ | Server | File |
52
+ |---|---|
53
+ | Cloudflare Worker (\`wrangler dev\`) | \`--env-file <worker dir>/.dev.vars\` |
54
+ | A server in a subfolder of a monorepo or an extension repo | \`--env-file <server dir>/.env.local\` |
55
+
56
+ Find the file by reading how that server loads its environment, not by its
57
+ framework's reputation. The CLI adds whatever file it writes to \`.gitignore\`
58
+ if git would commit it.
59
+
60
+ On a host that keeps secrets itself \u2014 \`wrangler secret put SOBA_KEY\`, Vercel's
61
+ or Railway's environment \u2014 production takes \`SOBA_PROD_KEY\`'s value under the
62
+ name \`SOBA_KEY\`. That is the person's to paste; tell them the command.
63
+
64
+ A development key reaches only the user's own AI \u2014 their machine, their
65
+ subscription, their API key. In production, \`SOBA_KEY\` is set to the value of
66
+ \`SOBA_PROD_KEY\` in the host's environment; say so to the person rather than
67
+ doing it.
68
+
69
+ If you cannot run commands but have a \`soba_create_key\` tool, call it: the
70
+ secret is returned once and nothing can show it again, so write it into
71
+ \`.env.local\` in the same step. Failing both, ask the person to run the command
72
+ above themselves. Do not leave \`SOBA_KEY\`
73
+ empty and do not invent a value; both fail at the first request, and the first
74
+ fails as an auth error rather than as a missing variable.
75
+
76
+ Put \`SOBA_KEY\` in \`.env.local\` (or the server's own file, above), and check
77
+ that file is gitignored before writing it. Never in \`.env\`, never in \`.env.example\`, never in anything
78
+ tracked, and never echoed back to the terminal.
79
+
80
+ ### 2. Mount the connect component
81
+
82
+ On the page where a user manages their own account, so they can connect a
83
+ machine of their own:
84
+
85
+ \`\`\`jsx
86
+ import { SobaProvider, ConnectCompute } from "@soba-so/react";
87
+
88
+ // Wherever a user manages their own account.
89
+ <SobaProvider sessionEndpoint="/api/soba/session">
90
+ <ConnectCompute />
91
+ </SobaProvider>
92
+ \`\`\`
93
+
94
+ Back it with \`POST /api/soba/session\`, a route in the customer's own app that
95
+ proxies \`POST /v1/end_users/session\` using the secret key and the same
96
+ server-resolved user id. It returns a short-lived \`est_\` credential. The
97
+ browser never sees \`SOBA_KEY\`.
98
+
99
+ Alongside it, \`<ComputeStatus />\`: asleep, offline, signed out of its CLI, or
100
+ serving a run right now. When the compute is the user's own machine, the state of
101
+ that machine is user-facing: they are the only one who can do anything about it.
102
+
103
+ If there is no settings page yet, build one. Do not point the connect return URL
104
+ at a page that does not render \`<ConnectCompute />\`.
105
+
106
+ ## The rest of the components, which are not optional in practice
107
+
108
+ A settings page is where somebody checks on a machine they already attached. It
109
+ is not where they decide to attach one, because nobody visits it wondering
110
+ whether to. An app that mounts only \`<ConnectCompute />\` there has nowhere for a
111
+ run to land until a user happens to wander in, and meets \`no_compute\` first.
112
+
113
+ Put the decision where it is made. All of these come from \`@soba-so/react\`, and
114
+ all of them raise the same modal \u2014 the provider mounts exactly one, so an app
115
+ with all of them cannot put two pairings on screen at once.
116
+
117
+ | Component | Where |
118
+ |---|---|
119
+ | \`<ModelPicker />\` | In the composer, beside the send button. Which of THEIR models answers |
120
+ | \`<RunLimit />\` | Where a run just failed for a reason the user can fix. Render it in the place the answer was going to be |
121
+ | \`<RunError />\` | Beside it, for every other failure. The two never both speak |
122
+ | \`<ConnectNudge />\` | While the allowance is running down and they still have a choice |
123
+ | \`<SobaBilling />\` | The current plan, then the grid. The billing page you probably want |
124
+ | \`<SobaPlans showConnect />\` | A pricing page with no current-plan card on it |
125
+ | \`<SobaCurrentPlan />\` | The card alone, when composing the page yourself |
126
+ | \`<SobaCheckout />\` | Takes the payment and grants the entitlement |
127
+ | \`<SobaGate feature="workspaces" />\` | Around anything a plan limits \u2014 a "New workspace" button, an export. Shows it while allowed, otherwise the reason and the plans |
128
+
129
+ A gate draws the button; it does not secure the action. On the server, ask
130
+ before you create: \`soba.users.check(user, "workspaces", { use: 1 })\` answers
131
+ \`{ allowed, reason }\` and counts the use in the same call, so two requests for
132
+ the last one cannot both get it. Record use you did not ask about with
133
+ \`soba.users.track(user, "workspaces", -1)\` when one is deleted.
134
+
135
+ Prices, allowances and the connected reward come from the plan objects, so
136
+ changing them later is not a deploy. Pass \`showConnect\` only on a page with no
137
+ current-plan card: a bring-your-own plan would otherwise state a requirement with
138
+ no way to meet it.
139
+
140
+ ## Components or hooks: decide this by looking at the repo
141
+
142
+ Both halves ship. **Every component above is one rendering of an exported hook,
143
+ and the components derive nothing of their own** \u2014 so what you build from a hook
144
+ and what we draw cannot disagree. Which half to reach for is not a matter of
145
+ taste, and not a question to ask the person. Open the files first:
146
+
147
+ **The app has no surface for this yet** \u2014 no settings page, no pricing page, a
148
+ composer with nothing in its footer \u2014 then **mount the component.** It brings
149
+ its own stylesheet, reads from the provider, and there is nothing left to
150
+ decide. Do not build a settings page by hand so that you can fill it with hooks.
151
+
152
+ **The app already has its own** \u2014 a chat composer with its own controls, a
153
+ billing page in its own cards, a design system, Tailwind classes everywhere,
154
+ anything that plainly belongs to somebody's product \u2014 then **import the hook and
155
+ keep their markup.** Our CSS will not match the screen it lands on, and a card
156
+ of ours in the middle of their chat reads as something the assistant said. The
157
+ rules are ours; the pixels stay theirs.
158
+
159
+ | Component | Hook | What the hook decides |
160
+ |---|---|---|
161
+ | \`<ModelPicker />\` | \`useModel()\` | Which models this person could be answered by, the one in force, and the two fields to put on the run |
162
+ | \`<RunLimit />\` | \`useRunLimit()\` | The two failures a person can act on, and the offer that answers each |
163
+ | \`<RunError />\` | \`runErrorMessage()\` | What to say about a failure, or null when it is not this one's to say |
164
+ | \`<ConnectNudge />\` | \`useConnectNudge()\` | Whether there is anything worth saying at all, which of four reasons, and how big |
165
+ | \`<ComputeStatus />\` | \`useCompute()\` | The machines in words, and "nothing connected" versus "we have not looked yet" |
166
+ | \`<ConnectButton />\` | \`useConnectButton()\` | What the control says once something is attached, and what pressing it means |
167
+ | \`<SobaPlans />\` | \`usePlans()\` | The catalogue as decisions: price, allowance, compute, the connect row, one \`take()\` per plan |
168
+ | \`<SobaCurrentPlan />\` | \`useCurrentPlan()\` | The allowance arithmetic and the one sentence that is true of it |
169
+ | \`<SobaCheckout />\` | \`useCheckout()\` | The session, the label, and the rule that nothing is granted in the browser |
170
+ | \`<SobaGate />\` | \`useFeature()\` | Whether this person may use a feature, and why not: no plan, plan ended, not included, limit reached |
171
+
172
+ \`<ConnectCompute />\`, \`<SobaBilling />\` and \`<ConnectModal />\` are compositions
173
+ of the above and have no rule of their own. \`<ConnectPill />\` is the half of
174
+ \`<ConnectButton />\` that reports \u2014 the mark, the state and the machine, with no
175
+ popover under it and no flow behind it \u2014 for an app putting the readout in its
176
+ own header and opening its own panel. Pressing it is yours to wire; everything
177
+ it says is \`connectSays\`. \`<SobaProvider>\` is needed either way: the hooks
178
+ read from it, so a hook with no provider above it is the one mistake this split
179
+ invites.
180
+
181
+ Two belong to no component of their own. \`useOpenSobaConnect()\` brings the
182
+ Soba app forward on this person's machine and hands back the one honest thing a
183
+ page can say about having tried \u2014 a browser cannot tell whether a \`soba://\`
184
+ link was answered, and a page that claims it worked is guessing.
185
+ \`useMachineRuns()\` is the "Running now" list out of \`<ConnectButton />\`'s
186
+ panel with no panel around it: what this person's machines are running FOR THIS
187
+ APP, and \`stop(runId)\` to end one as the person whose machine it is on. The app
188
+ that started that run sees it finish with \`reason: "stopped_by_user"\`, which is
189
+ the point: a surface built on this is a kill switch its owner holds, not a
190
+ request to somebody's server.
191
+
192
+ \`useRedeemCode()\` is a promo code field's behaviour: \`redeem(code)\` gives the
193
+ user what the app's reward hands out \u2014 AI usage on top of their plan, a plan for
194
+ some days, or a discount at their next checkout \u2014 and re-reads their plan, so
195
+ the allowance on screen moves without a reload. Its \`error\` is a sentence ready
196
+ to show. Put the field where somebody arrives with a code, usually the billing
197
+ page. \`<RedeemCode />\` is that field already drawn \u2014 an input and an Apply
198
+ button \u2014 for an app with no billing page of its own to put one in.
199
+
200
+ \`useSoba()\` is the raw provider state. Reach for a named hook first and use
201
+ this one for what no hook covers \u2014 \`run()\`, \`connect.open()\`,
202
+ \`plan.refresh()\`. A page that reads \`compute.connected\` or \`plan.remaining\`
203
+ straight off it is re-deriving a decision that is already exported, and the two
204
+ copies are how our own apps drifted.
205
+
206
+ ### What not to write yourself
207
+
208
+ These are not style preferences. Each one is a judgement this package already
209
+ makes, and each has been got wrong in production \u2014 in our own code, which is why
210
+ they are exported at all. If you find yourself writing one of them, delete it
211
+ and import the answer:
212
+
213
+ - **A list of model names.** The names come off the person's own machine, so a
214
+ list in your code refuses the model they can see on their own screen.
215
+ \`useModel()\` reads it from what they have connected.
216
+ - **A model id on a run without its agent.** Soba applies a person's own agent
217
+ preference only to a run whose model is blank or a size, so a named model
218
+ travelling alone goes to whichever agent is found first. Spread
219
+ \`useModel().run\` whole \u2014 it is \`{ model, runtime? }\` \u2014 and have your route
220
+ forward both fields.
221
+ - **"Nothing connected yet."** That is a claim about somebody's account and it
222
+ is only true once the account has been read. Said while the first request is
223
+ in flight, or after it failed, it tells a person with a paired laptop that it
224
+ has vanished. \`useCompute().view\` is the four-way answer.
225
+ - **A match on an error message.** \`useRunLimit\` and \`runErrorMessage\` read the
226
+ CODE. Messages are prose we rewrite, and matching on them breaks on a copy
227
+ edit.
228
+ - **Words for a cost class.** \`tierLabel(tier)\` is the one glossary for the five
229
+ economics this product is about. Four hand-written copies in this repo
230
+ disagreed with each other, and three reported somebody's own API key as
231
+ frontier supply the app had paid for.
232
+ - **Where the pick is remembered, or how a stale one is handled.** A model whose
233
+ machine is gone falls back to Automatic and stays picked. \`useModel()\` does
234
+ that and tells you with \`unavailable\`.
235
+
236
+ Read \`https://soba.so/docs/components\` for each hook's full shape.
237
+
238
+ ## Reaching Claude Code and Codex
239
+
240
+ \`chat/completions\` is stateless and the caller owns the agent loop, so it
241
+ reaches models. Reaching a user's own Claude or ChatGPT subscription means
242
+ letting Soba own the loop instead:
243
+
244
+ \`\`\`js
245
+ import { Soba } from "@soba-so/sdk";
246
+ import { sobaUserId } from "@soba-so/sdk/identity";
247
+
248
+ const soba = new Soba({ apiKey: process.env.SOBA_KEY });
249
+
250
+ const run = await soba.run({
251
+ // Derived from something that outlives your database. A primary key
252
+ // minted at sign-up is not: reset it and the same person arrives as
253
+ // somebody new, stranded from the machine they paired.
254
+ // https://soba.so/docs/identity
255
+ user: sobaUserId(session.user.username),
256
+ model: "auto",
257
+ prompt: "Summarise everything I saved this week",
258
+ tools: [listNotes, readNote], // your code, executed in your app
259
+ });
260
+ \`\`\`
261
+
262
+ Tools passed to a run execute in the customer's own process, so they can read
263
+ the app's own data. The model asks for data by calling them; the data does not
264
+ travel in the prompt.
265
+
266
+ ## Proving it
267
+
268
+ \`soba/echo\` is answered by Soba itself \u2014 no machine, no metered supply,
269
+ nothing billed \u2014 and still proves the key, the base URL and the \`user\` field:
270
+
271
+ \`\`\`sh
272
+ curl https://api.soba.so/v1/chat/completions \\
273
+ -H "Authorization: Bearer $SOBA_KEY" \\
274
+ -H "Content-Type: application/json" \\
275
+ -d '{ "model": "soba/echo", "user": "usr_123",
276
+ "messages": [{ "role": "user", "content": "ping" }] }'
277
+ \`\`\`
278
+
279
+ A curl is evidence the credential works. It is NOT evidence the app is
280
+ integrated \u2014 the dashboard's integrate step ticks on a request arriving from the
281
+ real call site, and it cannot tell a curl from a deployment. Do not send one to
282
+ make a checklist go green.
283
+
284
+ ## A new project rather than an existing one
285
+
286
+ There is no separate procedure for this and no question to ask the person: look
287
+ at the directory. If there is no app yet, run \`npx create-soba@latest\` and let
288
+ it write one \u2014 it ships a correct \`/api/soba/session\`, which is the security
289
+ boundary of the whole integration, and a plausible hand-written one is the
290
+ failure mode that matters. Then the two edits above are already done and what
291
+ remains is the key.
292
+
293
+ ## What is not yours to do
294
+
295
+ Pairing is a person installing the Soba app on their own laptop and approving
296
+ it. Pricing, allowances and rewards are decisions about somebody's business.
297
+ Do neither on their behalf; say what is left and stop.`;var C=2.5*1e6,d=[{upTo:null,microsPerUnit:C}];var Ae=[{id:"hobby",name:"Hobby",feeMicros:19*1e6,includedActivations:12,seatLimit:2,appLimit:3,bands:d,features:["All three envelope dials: duration, volume and feature scope","3 projects","2 seats","Owner and Member roles","Activation counts","Docs support"]},{id:"starter",seatLimit:null,appLimit:null,name:"Starter",feeMicros:199*1e6,includedActivations:90,bands:d,features:["Unlimited projects","Unlimited seats","Admin and Billing roles","Cohort and conversion attribution","Email support"]},{id:"growth",seatLimit:null,appLimit:null,name:"Growth",feeMicros:599*1e6,includedActivations:300,bands:d,features:["Window A/B experimentation","Email support, one business day"]},{id:"scale",seatLimit:null,appLimit:null,name:"Scale",feeMicros:1999*1e6,includedActivations:1500,bands:d,features:["Custom roles and audit log","Effective-vs-configured window analytics","Shared channel with an SLA"]}];var p=[{name:"set_up",title:"Set Soba up in this repo",description:"Works through Soba's setup checklist one step at a time, editing this repo and showing you each diff.",text:`Set this project up on Soba, working from the real checklist rather than from memory.
298
+
299
+ The loop, and keep to it:
300
+
301
+ 1. Call \`soba_status\`. That is the truth about what is done; nothing you did
302
+ earlier in this conversation counts until it shows up there.
303
+ 2. Call \`soba_next_step\` for the first outstanding one.
304
+ 3. Do it. The repo is yours to read and edit with your own tools \u2014 find the
305
+ actual LLM call sites rather than assuming where they are, and show me the
306
+ diff before you write anything.
307
+ 4. Call \`soba_status\` again. If the step did not flip, say so and work out why
308
+ instead of moving on. A step that silently did not take is the failure this
309
+ whole checklist exists to catch.
310
+ 5. Repeat until it says complete.
311
+
312
+ Three things not to do:
313
+
314
+ - Do not invent a plan. Pricing, allowances and rewards are decisions about my
315
+ business \u2014 ask me, and only then call \`soba_create_plan\`.
316
+ - Do not put the secret key anywhere but \`.env.local\`, and check that file is
317
+ gitignored before you write it. Do not echo it back to the terminal.
318
+ - Do not guess the connect return URL. It has to be a page that actually renders
319
+ \`<ConnectCompute />\`; if there isn't one yet, build it first.
320
+
321
+ Start with \`soba_status\`.
322
+
323
+ ---
324
+
325
+ What the codebase steps actually involve, so you are not working from memory
326
+ there either:
327
+
328
+ ${w}`}],_=new Map(p.map(n=>[n.name,n]));function a(n){process.stderr.write(`soba-mcp: ${n}
329
+ `)}function x(n){process.stdout.write(`${JSON.stringify(n)}
330
+ `)}function k(n){let t="",e=new Set,r=!1,s=o=>{e.add(o),o.finally(()=>{e.delete(o),r&&e.size===0&&process.exit(0)})};process.stdin.setEncoding("utf8"),process.stdin.on("data",o=>{t+=o;let i=t.indexOf(`
331
+ `);for(;i!==-1;){let l=t.slice(0,i).trim();t=t.slice(i+1),l&&s(I(l,n)),i=t.indexOf(`
332
+ `)}}),process.stdin.on("end",()=>{r=!0,e.size===0&&process.exit(0),setTimeout(()=>process.exit(0),1e4).unref()})}async function I(n,t){let e;try{e=JSON.parse(n)}catch{a("ignored a line that was not json");return}let r=e.id===void 0||e.id===null;try{let s=await t(e.method,e.params??{});r||x({jsonrpc:"2.0",id:e.id,result:s})}catch(s){if(r)return;let o=s instanceof Error?s.message:String(s),i=o==="method not found"?-32601:-32603;x({jsonrpc:"2.0",id:e.id,error:{code:i,message:o}})}}var v={type:"object",properties:{},additionalProperties:!1};function S(n){let t=n.steps.map(r=>`${r.done?"[x]":"[ ]"} ${r.title} \u2014 ${r.state}`),e=n.complete?"Every step is done. Nothing here needs you again.":`Next: ${n.next}. Call soba_next_step for what to do.`;return`${t.join(`
333
+ `)}
334
+
335
+ ${e}`}function P(n,t){return n.steps.find(e=>e.id===t)}var m=[{name:"soba_status",title:"Read the setup checklist",description:"Soba's setup checklist for this app and which steps are done. Call this first, and again after every change, so you are acting on what is true rather than on what you did last.",inputSchema:v,run:async n=>S(await n.get("/v1/manage/progress"))},{name:"soba_next_step",title:"What to do next",description:"The first outstanding step, with an instruction written for you. Do what it says using your own tools \u2014 read the repo, make the edits, show the diff. Then call soba_status again to confirm it took.",inputSchema:v,run:async n=>{let t=await n.get("/v1/manage/progress");if(t.complete)return"Setup is complete. There is nothing to do.";let e=P(t,t.next);return e?e.effect==="human"?e.id==="price"?`Next: ${e.title}
336
+
337
+ ${e.lede}
338
+
339
+ This one is not yours to decide. Ask what the plan should cost, what it includes, and what connecting a machine earns back \u2014 then call soba_create_plan with the answers.`:e.id==="run"?`Next: ${e.title}
340
+
341
+ ${e.lede}
342
+
343
+ There is nothing to do here and nothing to press. This step ticks when a request arrives from the app's own call site, so the way to finish it is to run the app and use it. A curl will not do it: the gateway stamps a key at authentication and cannot tell one from a deployment.`:`Next: ${e.title}
344
+
345
+ ${e.lede}
346
+
347
+ This one is done in the dashboard, not from here \u2014 there is no tool on this server for it. Tell whoever asked to open the Set up page and take this step, then call soba_status to confirm it landed.`:`Next: ${e.title} (${e.effect})
348
+
349
+ ${e.instruction??e.lede}
350
+
351
+ When you are done, call soba_status to confirm it registered.`:S(t)}},{name:"soba_create_key",title:"Mint a secret key",description:"Mints this app's secret key and returns it ONCE. Write it straight into the project's .env.local as SOBA_KEY, confirm .env.local is gitignored, and do not print it back to the terminal or put it in a file that is committed.",inputSchema:{type:"object",properties:{name:{type:"string",description:"What to call it in the dashboard's key list."},environment:{type:"string",enum:["development","production"],description:"Defaults to production, like the dashboard. Only say development if this key is for a machine that never serves real users."}},additionalProperties:!1},run:async(n,t)=>{let e=await n.post("/v1/manage/keys",{name:t.name??"MCP",environment:t.environment});return`Created ${e.preview} (${e.environment}).
352
+
353
+ SOBA_KEY=${e.key}
354
+
355
+ This is the only time it exists. Put it in .env.local now.`}},{name:"soba_set_connect_url",title:"Point Connect back at the app",description:"Sets where someone who started pairing in their terminal gets sent to sign in. It must be the public https page that renders <ConnectCompute />. If no such page exists yet, do not guess one \u2014 build the page first.",inputSchema:{type:"object",properties:{url:{type:"string",description:"The https URL of the page. Loopback is allowed in development."}},required:["url"],additionalProperties:!1},run:async(n,t)=>(await n.patch("/v1/manage/app",{connect_return_url:t.url}),`Connect now returns to ${t.url}.`)},{name:"soba_create_plan",title:"Create a plan",description:"Creates a plan: the price, the allowance, what a run may fall back to, and what connecting a machine earns back. Only call this with numbers a person gave you. Do not invent a price.",inputSchema:{type:"object",properties:{name:{type:"string"},description:{type:"string"},price:{type:"number",description:"Dollars a month. 0 is a free plan."},unit:{type:"string",enum:["run","token","cost"],description:"What both allowances are counted in. `cost` counts hundredths of a cent (10,000 to the dollar); a token or cost overage price is per 1M tokens or per $1."},included_units:{type:["number","null"],description:"Runs a month on AI the app pays for (open-weights, frontier). Null is unlimited."},trial_days:{type:"number"},route_prefer:{type:"array",items:{type:"string",enum:["user-subscription","user-hardware","open-weights","frontier"]},description:"What a run may use, best first. This is also the ceiling: a class left out can never be billed to this app."},max_cost_per_run:{type:["number","null"],description:"Dollars one run may cost."},own_included_units:{type:["number","null"],description:"Runs a month on the user's own AI (their machine, subscription or key), counted apart from included_units. Null is unlimited."},own_unit:{type:["string","null"],enum:["run","token","credit","task",null],description:"What own_included_units counts, when not unit: a hybrid plan gives 200 runs on the user's own AI and 2,000 credits on yours. Null means the same as unit."},connected_reward:{type:"string",enum:["none","included","credit"]},connected_included_units:{type:["number","null"]},connected_credit:{type:["number","null"]}},required:["name","price","route_prefer"],additionalProperties:!1},run:async(n,t)=>{let e=await n.post("/v1/manage/plans",t);return`Created the plan "${e.name}"${e.id?` (${e.id})`:""}.`}},{name:"soba_give_reward",title:"Give a user a reward",description:"Gives one user a reward the app already has \u2014 money off, extra AI usage, or a plan for some days. Once per user per reward. Only call this when a person asked for that user to get it.",inputSchema:{type:"object",properties:{reward:{type:"string",description:"The reward's ID, e.g. early_bird."},user:{type:"string",description:"The app's own id for the user."}},required:["reward","user"],additionalProperties:!1},run:async(n,t)=>(await n.post("/v1/manage/rewards/give",t),`Gave ${t.user} the reward ${t.reward}.`)}],A=new Map(m.map(n=>[n.name,n]));var O="https://soba.so",U="2025-06-18",N="0.0.3";function L(n){let t={token:process.env.SOBA_MANAGEMENT_KEY??null,baseUrl:process.env.SOBA_MANAGE_URL??O};for(let e=0;e<n.length;e++){let r=n[e];r==="--token"?t.token=n[++e]??null:r?.startsWith("--token=")?t.token=r.slice(8):r==="--url"?t.baseUrl=n[++e]??t.baseUrl:r?.startsWith("--url=")&&(t.baseUrl=r.slice(6))}return t.baseUrl=t.baseUrl.replace(/\/+$/,""),t}var h=L(process.argv.slice(2)),g=h.token;g||(a("no management key. Pass --token mk_soba_\u2026 or set SOBA_MANAGEMENT_KEY."),a("Mint one in your Soba dashboard, on the Set up page."),process.exit(2));g.startsWith("mk_soba_")||(a("that is not a management key. It starts with mk_soba_, and sk_soba_ is a"),a("different credential \u2014 your app's secret key. Do not paste that here."),process.exit(2));var D=new c(g,h.baseUrl);k(async(n,t)=>{switch(n){case"initialize":return{protocolVersion:U,capabilities:{tools:{},prompts:{}},serverInfo:{name:"soba",version:N},instructions:"Soba's setup checklist. Call soba_status first, then soba_next_step, and do what it says with your own tools \u2014 the repo is yours to edit, not this server's. Call soba_status again afterwards to confirm the step registered rather than assuming it did."};case"notifications/initialized":case"notifications/cancelled":return{};case"ping":return{};case"prompts/list":return{prompts:p.map(e=>({name:e.name,title:e.title,description:e.description}))};case"prompts/get":{let e=typeof t.name=="string"?t.name:"",r=_.get(e);if(!r)throw new Error(`no such prompt: ${e}`);return{description:r.description,messages:[{role:"user",content:{type:"text",text:r.text}}]}}case"tools/list":return{tools:m.map(e=>({name:e.name,title:e.title,description:e.description,inputSchema:e.inputSchema}))};case"tools/call":{let e=typeof t.name=="string"?t.name:"",r=A.get(e);if(!r)return f(`no such tool: ${e}`);let s=t.arguments??{};try{return{content:[{type:"text",text:await r.run(D,s)}]}}catch(o){return o instanceof u?f(F(o)):f(o instanceof Error?o.message:String(o))}}default:throw new Error("method not found")}});function f(n){return{content:[{type:"text",text:n}],isError:!0}}function F(n){return n.status===401?"Soba refused the management key. It may have been revoked, or it may belong to a different app. Mint a new one on the Set up page.":n.message}a(`ready \u2014 ${m.length} tools, ${p.length} prompt against ${h.baseUrl}`);
package/package.json CHANGED
@@ -1,6 +1,41 @@
1
1
  {
2
2
  "name": "@soba-so/mcp",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.0.3",
4
+ "description": "Sets Soba up from inside your coding agent: it reads the checklist, does the next step, and reads it back.",
5
+ "license": "UNLICENSED",
6
+ "homepage": "https://soba.so/docs/quickstart",
7
+ "type": "module",
8
+ "bin": {
9
+ "soba-mcp": "dist/mcp.mjs"
10
+ },
11
+ "files": [
12
+ "dist/mcp.mjs",
13
+ "README.md"
14
+ ],
15
+ "scripts": {
16
+ "build": "node ../../scripts/build-mcp.mjs",
17
+ "prepublishOnly": "node ../../scripts/build-mcp.mjs",
18
+ "typecheck": "tsc -p tsconfig.json"
19
+ },
20
+ "devDependencies": {
21
+ "@soba-platform/core": "workspace:*"
22
+ },
23
+ "engines": {
24
+ "node": ">=22"
25
+ },
26
+ "keywords": [
27
+ "soba",
28
+ "mcp",
29
+ "claude",
30
+ "setup",
31
+ "byoc"
32
+ ],
33
+ "publishConfig": {
34
+ "access": "public"
35
+ },
36
+ "repository": {
37
+ "type": "git",
38
+ "url": "git+https://github.com/andreachello/soba.git",
39
+ "directory": "packages/soba-mcp"
40
+ }
41
+ }