@soba-so/mcp 0.0.0-stage → 0.0.2

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 +292 -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://www.soba.so`. |
73
+
74
+ No runtime dependencies. Node 22+.
package/dist/mcp.mjs ADDED
@@ -0,0 +1,292 @@
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 Q=60*6e4;var M=864e5,E=30,ne={abandonedAfterMs:E*M};var oe=30*24*60*60*1e3;var se=5*60*1e3;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 you have a \`soba_create_key\` tool, call it: the secret is returned once and
33
+ nothing can show it again, so write it into \`.env.local\` in the same step. If
34
+ you do not, ask the person for it from their dashboard's Keys page \u2014 where they
35
+ can also rotate one if they no longer have the secret. Do not leave \`SOBA_KEY\`
36
+ empty and do not invent a value; both fail at the first request, and the first
37
+ fails as an auth error rather than as a missing variable.
38
+
39
+ Put \`SOBA_KEY\` in \`.env.local\`, and check that file is gitignored before
40
+ writing it. Never in \`.env\`, never in \`.env.example\`, never in anything
41
+ tracked, and never echoed back to the terminal.
42
+
43
+ ### 2. Mount the connect component
44
+
45
+ On the page where a user manages their own account, so they can connect a
46
+ machine of their own:
47
+
48
+ \`\`\`jsx
49
+ import { SobaProvider, ConnectCompute } from "@soba-so/react";
50
+
51
+ // Wherever a user manages their own account.
52
+ <SobaProvider sessionEndpoint="/api/soba/session">
53
+ <ConnectCompute />
54
+ </SobaProvider>
55
+ \`\`\`
56
+
57
+ Back it with \`POST /api/soba/session\`, a route in the customer's own app that
58
+ proxies \`POST /v1/end_users/session\` using the secret key and the same
59
+ server-resolved user id. It returns a short-lived \`est_\` credential. The
60
+ browser never sees \`SOBA_KEY\`.
61
+
62
+ Alongside it, \`<ComputeStatus />\`: asleep, offline, signed out of its CLI, or
63
+ serving a run right now. When the compute is the user's own machine, the state of
64
+ that machine is user-facing: they are the only one who can do anything about it.
65
+
66
+ If there is no settings page yet, build one. Do not point the connect return URL
67
+ at a page that does not render \`<ConnectCompute />\`.
68
+
69
+ ## The rest of the components, which are not optional in practice
70
+
71
+ A settings page is where somebody checks on a machine they already attached. It
72
+ is not where they decide to attach one, because nobody visits it wondering
73
+ whether to. An app that mounts only \`<ConnectCompute />\` there has nowhere for a
74
+ run to land until a user happens to wander in, and meets \`no_compute\` first.
75
+
76
+ Put the decision where it is made. All of these come from \`@soba-so/react\`, and
77
+ all of them raise the same modal \u2014 the provider mounts exactly one, so an app
78
+ with all of them cannot put two pairings on screen at once.
79
+
80
+ | Component | Where |
81
+ |---|---|
82
+ | \`<ModelPicker />\` | In the composer, beside the send button. Which of THEIR models answers |
83
+ | \`<RunLimit />\` | Where a run just failed for a reason the user can fix. Render it in the place the answer was going to be |
84
+ | \`<RunError />\` | Beside it, for every other failure. The two never both speak |
85
+ | \`<ConnectNudge />\` | While the allowance is running down and they still have a choice |
86
+ | \`<SobaBilling />\` | The current plan, then the grid. The billing page you probably want |
87
+ | \`<SobaPlans showConnect />\` | A pricing page with no current-plan card on it |
88
+ | \`<SobaCurrentPlan />\` | The card alone, when composing the page yourself |
89
+ | \`<SobaCheckout />\` | Takes the payment and grants the entitlement |
90
+
91
+ Prices, allowances and the connected reward come from the plan objects, so
92
+ changing them later is not a deploy. Pass \`showConnect\` only on a page with no
93
+ current-plan card: a bring-your-own plan would otherwise state a requirement with
94
+ no way to meet it.
95
+
96
+ ## Components or hooks: decide this by looking at the repo
97
+
98
+ Both halves ship. **Every component above is one rendering of an exported hook,
99
+ and the components derive nothing of their own** \u2014 so what you build from a hook
100
+ and what we draw cannot disagree. Which half to reach for is not a matter of
101
+ taste, and not a question to ask the person. Open the files first:
102
+
103
+ **The app has no surface for this yet** \u2014 no settings page, no pricing page, a
104
+ composer with nothing in its footer \u2014 then **mount the component.** It brings
105
+ its own stylesheet, reads from the provider, and there is nothing left to
106
+ decide. Do not build a settings page by hand so that you can fill it with hooks.
107
+
108
+ **The app already has its own** \u2014 a chat composer with its own controls, a
109
+ billing page in its own cards, a design system, Tailwind classes everywhere,
110
+ anything that plainly belongs to somebody's product \u2014 then **import the hook and
111
+ keep their markup.** Our CSS will not match the screen it lands on, and a card
112
+ of ours in the middle of their chat reads as something the assistant said. The
113
+ rules are ours; the pixels stay theirs.
114
+
115
+ | Component | Hook | What the hook decides |
116
+ |---|---|---|
117
+ | \`<ModelPicker />\` | \`useModel()\` | Which models this person could be answered by, the one in force, and the two fields to put on the run |
118
+ | \`<RunLimit />\` | \`useRunLimit()\` | The two failures a person can act on, and the offer that answers each |
119
+ | \`<RunError />\` | \`runErrorMessage()\` | What to say about a failure, or null when it is not this one's to say |
120
+ | \`<ConnectNudge />\` | \`useConnectNudge()\` | Whether there is anything worth saying at all, which of four reasons, and how big |
121
+ | \`<ComputeStatus />\` | \`useCompute()\` | The machines in words, and "nothing connected" versus "we have not looked yet" |
122
+ | \`<ConnectButton />\` | \`useConnectButton()\` | What the control says once something is attached, and what pressing it means |
123
+ | \`<SobaPlans />\` | \`usePlans()\` | The catalogue as decisions: price, allowance, compute, the connect row, one \`take()\` per plan |
124
+ | \`<SobaCurrentPlan />\` | \`useCurrentPlan()\` | The allowance arithmetic and the one sentence that is true of it |
125
+ | \`<SobaCheckout />\` | \`useCheckout()\` | The session, the label, and the rule that nothing is granted in the browser |
126
+
127
+ \`<ConnectCompute />\`, \`<SobaBilling />\` and \`<ConnectModal />\` are compositions
128
+ of the above and have no rule of their own. \`<SobaProvider>\` is needed either
129
+ way: the hooks read from it, so a hook with no provider above it is the one
130
+ mistake this split invites.
131
+
132
+ One more belongs to no component: \`useOpenSobaConnect()\`, which brings the
133
+ Soba app forward on this person's machine and hands back the one honest thing a
134
+ page can say about having tried \u2014 a browser cannot tell whether a \`soba://\`
135
+ link was answered, and a page that claims it worked is guessing.
136
+
137
+ \`useSoba()\` is the raw provider state. Reach for a named hook first and use
138
+ this one for what no hook covers \u2014 \`run()\`, \`connect.open()\`,
139
+ \`plan.refresh()\`. A page that reads \`compute.connected\` or \`plan.remaining\`
140
+ straight off it is re-deriving a decision that is already exported, and the two
141
+ copies are how our own apps drifted.
142
+
143
+ ### What not to write yourself
144
+
145
+ These are not style preferences. Each one is a judgement this package already
146
+ makes, and each has been got wrong in production \u2014 in our own code, which is why
147
+ they are exported at all. If you find yourself writing one of them, delete it
148
+ and import the answer:
149
+
150
+ - **A list of model names.** The names come off the person's own machine, so a
151
+ list in your code refuses the model they can see on their own screen.
152
+ \`useModel()\` reads it from what they have connected.
153
+ - **A model id on a run without its agent.** Soba applies a person's own agent
154
+ preference only to a run whose model is blank or a size, so a named model
155
+ travelling alone goes to whichever agent is found first. Spread
156
+ \`useModel().run\` whole \u2014 it is \`{ model, runtime? }\` \u2014 and have your route
157
+ forward both fields.
158
+ - **"Nothing connected yet."** That is a claim about somebody's account and it
159
+ is only true once the account has been read. Said while the first request is
160
+ in flight, or after it failed, it tells a person with a paired laptop that it
161
+ has vanished. \`useCompute().view\` is the four-way answer.
162
+ - **A match on an error message.** \`useRunLimit\` and \`runErrorMessage\` read the
163
+ CODE. Messages are prose we rewrite, and matching on them breaks on a copy
164
+ edit.
165
+ - **Words for a cost class.** \`tierLabel(tier)\` is the one glossary for the five
166
+ economics this product is about. Four hand-written copies in this repo
167
+ disagreed with each other, and three reported somebody's own API key as
168
+ frontier supply the app had paid for.
169
+ - **Where the pick is remembered, or how a stale one is handled.** A model whose
170
+ machine is gone falls back to Automatic and stays picked. \`useModel()\` does
171
+ that and tells you with \`unavailable\`.
172
+
173
+ Read \`https://soba.so/docs/components\` for each hook's full shape.
174
+
175
+ ## Reaching Claude Code and Codex
176
+
177
+ \`chat/completions\` is stateless and the caller owns the agent loop, so it
178
+ reaches models. Reaching a user's own Claude or ChatGPT subscription means
179
+ letting Soba own the loop instead:
180
+
181
+ \`\`\`js
182
+ import { Soba } from "@soba-so/sdk";
183
+ import { sobaUserId } from "@soba-so/sdk/identity";
184
+
185
+ const soba = new Soba({ apiKey: process.env.SOBA_KEY });
186
+
187
+ const run = await soba.run({
188
+ // Derived from something that outlives your database. A primary key
189
+ // minted at sign-up is not: reset it and the same person arrives as
190
+ // somebody new, stranded from the machine they paired.
191
+ // https://soba.so/docs/identity
192
+ user: sobaUserId(session.user.username),
193
+ model: "auto",
194
+ prompt: "Summarise everything I saved this week",
195
+ tools: [listNotes, readNote], // your code, executed in your app
196
+ });
197
+ \`\`\`
198
+
199
+ Tools passed to a run execute in the customer's own process, so they can read
200
+ the app's own data. The model asks for data by calling them; the data does not
201
+ travel in the prompt.
202
+
203
+ ## Proving it
204
+
205
+ \`soba/echo\` is answered by Soba itself \u2014 no machine, no metered supply,
206
+ nothing billed \u2014 and still proves the key, the base URL and the \`user\` field:
207
+
208
+ \`\`\`sh
209
+ curl https://api.soba.so/v1/chat/completions \\
210
+ -H "Authorization: Bearer $SOBA_KEY" \\
211
+ -H "Content-Type: application/json" \\
212
+ -d '{ "model": "soba/echo", "user": "usr_123",
213
+ "messages": [{ "role": "user", "content": "ping" }] }'
214
+ \`\`\`
215
+
216
+ A curl is evidence the credential works. It is NOT evidence the app is
217
+ integrated \u2014 the dashboard's integrate step ticks on a request arriving from the
218
+ real call site, and it cannot tell a curl from a deployment. Do not send one to
219
+ make a checklist go green.
220
+
221
+ ## A new project rather than an existing one
222
+
223
+ There is no separate procedure for this and no question to ask the person: look
224
+ at the directory. If there is no app yet, run \`npx create-soba@latest\` and let
225
+ it write one \u2014 it ships a correct \`/api/soba/session\`, which is the security
226
+ boundary of the whole integration, and a plausible hand-written one is the
227
+ failure mode that matters. Then the two edits above are already done and what
228
+ remains is the key.
229
+
230
+ ## What is not yours to do
231
+
232
+ Pairing is a person installing the Soba app on their own laptop and approving
233
+ it. Pricing, allowances and rewards are decisions about somebody's business.
234
+ Do neither on their behalf; say what is left and stop.`;var T=2.5*1e6,d=[{upTo:null,microsPerUnit:T}];var me=[{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.
235
+
236
+ The loop, and keep to it:
237
+
238
+ 1. Call \`soba_status\`. That is the truth about what is done; nothing you did
239
+ earlier in this conversation counts until it shows up there.
240
+ 2. Call \`soba_next_step\` for the first outstanding one.
241
+ 3. Do it. The repo is yours to read and edit with your own tools \u2014 find the
242
+ actual LLM call sites rather than assuming where they are, and show me the
243
+ diff before you write anything.
244
+ 4. Call \`soba_status\` again. If the step did not flip, say so and work out why
245
+ instead of moving on. A step that silently did not take is the failure this
246
+ whole checklist exists to catch.
247
+ 5. Repeat until it says complete.
248
+
249
+ Three things not to do:
250
+
251
+ - Do not invent a plan. Pricing, allowances and rewards are decisions about my
252
+ business \u2014 ask me, and only then call \`soba_create_plan\`.
253
+ - Do not put the secret key anywhere but \`.env.local\`, and check that file is
254
+ gitignored before you write it. Do not echo it back to the terminal.
255
+ - Do not guess the connect return URL. It has to be a page that actually renders
256
+ \`<ConnectCompute />\`; if there isn't one yet, build it first.
257
+
258
+ Start with \`soba_status\`.
259
+
260
+ ---
261
+
262
+ What the codebase steps actually involve, so you are not working from memory
263
+ there either:
264
+
265
+ ${w}`}],_=new Map(p.map(n=>[n.name,n]));function a(n){process.stderr.write(`soba-mcp: ${n}
266
+ `)}function x(n){process.stdout.write(`${JSON.stringify(n)}
267
+ `)}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(`
268
+ `);for(;i!==-1;){let l=t.slice(0,i).trim();t=t.slice(i+1),l&&s(C(l,n)),i=t.indexOf(`
269
+ `)}}),process.stdin.on("end",()=>{r=!0,e.size===0&&process.exit(0),setTimeout(()=>process.exit(0),1e4).unref()})}async function C(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(`
270
+ `)}
271
+
272
+ ${e}`}function I(n,t){return n.steps.find(e=>e.id===t)}var h=[{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=I(t,t.next);return e?e.effect==="human"?e.id==="price"?`Next: ${e.title}
273
+
274
+ ${e.lede}
275
+
276
+ 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}
277
+
278
+ ${e.lede}
279
+
280
+ 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}
281
+
282
+ ${e.lede}
283
+
284
+ 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})
285
+
286
+ ${e.instruction??e.lede}
287
+
288
+ 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}).
289
+
290
+ SOBA_KEY=${e.key}
291
+
292
+ 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."},included_units:{type:["number","null"],description:"Runs included. 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."},connected_reward:{type:"string",enum:["none","unlimited","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})`:""}.`}}],A=new Map(h.map(n=>[n.name,n]));var P="https://www.soba.so",O="2025-06-18",N="0.0.2";function U(n){let t={token:process.env.SOBA_MANAGEMENT_KEY??null,baseUrl:process.env.SOBA_MANAGE_URL??P};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 m=U(process.argv.slice(2)),g=m.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 L=new c(g,m.baseUrl);k(async(n,t)=>{switch(n){case"initialize":return{protocolVersion:O,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:h.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(L,s)}]}}catch(o){return o instanceof u?f(D(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 D(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 ${h.length} tools, ${p.length} prompt against ${m.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.2",
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
+ }