@soba-so/mcp 0.0.2 → 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 +1 -1
  2. package/dist/mcp.mjs +80 -17
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -69,6 +69,6 @@ ceiling and a reward, and those are decisions about your business.
69
69
  | | |
70
70
  |---|---|
71
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`. |
72
+ | `--url` | The Soba instance. Defaults to `https://soba.so`. |
73
73
 
74
74
  No runtime dependencies. Node 22+.
package/dist/mcp.mjs CHANGED
@@ -1,5 +1,5 @@
1
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.
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
3
  Requests go to Soba instead of to a model provider; Soba routes each one to the
4
4
  machine or plan belonging to the user it names, meters it, and bills for it.
5
5
 
@@ -29,15 +29,52 @@ not throw \u2014 it silently pairs runs to the wrong person.
29
29
 
30
30
  ### Where the key comes from
31
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\`
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\`
36
73
  empty and do not invent a value; both fail at the first request, and the first
37
74
  fails as an auth error rather than as a missing variable.
38
75
 
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
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
41
78
  tracked, and never echoed back to the terminal.
42
79
 
43
80
  ### 2. Mount the connect component
@@ -87,6 +124,13 @@ with all of them cannot put two pairings on screen at once.
87
124
  | \`<SobaPlans showConnect />\` | A pricing page with no current-plan card on it |
88
125
  | \`<SobaCurrentPlan />\` | The card alone, when composing the page yourself |
89
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.
90
134
 
91
135
  Prices, allowances and the connected reward come from the plan objects, so
92
136
  changing them later is not a deploy. Pass \`showConnect\` only on a page with no
@@ -123,16 +167,35 @@ rules are ours; the pixels stay theirs.
123
167
  | \`<SobaPlans />\` | \`usePlans()\` | The catalogue as decisions: price, allowance, compute, the connect row, one \`take()\` per plan |
124
168
  | \`<SobaCurrentPlan />\` | \`useCurrentPlan()\` | The allowance arithmetic and the one sentence that is true of it |
125
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 |
126
171
 
127
172
  \`<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
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
133
182
  Soba app forward on this person's machine and hands back the one honest thing a
134
183
  page can say about having tried \u2014 a browser cannot tell whether a \`soba://\`
135
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.
136
199
 
137
200
  \`useSoba()\` is the raw provider state. Reach for a named hook first and use
138
201
  this one for what no hook covers \u2014 \`run()\`, \`connect.open()\`,
@@ -231,7 +294,7 @@ remains is the key.
231
294
 
232
295
  Pairing is a person installing the Soba app on their own laptop and approving
233
296
  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.
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.
235
298
 
236
299
  The loop, and keep to it:
237
300
 
@@ -265,11 +328,11 @@ there either:
265
328
  ${w}`}],_=new Map(p.map(n=>[n.name,n]));function a(n){process.stderr.write(`soba-mcp: ${n}
266
329
  `)}function x(n){process.stdout.write(`${JSON.stringify(n)}
267
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(`
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(`
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(`
270
333
  `)}
271
334
 
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}
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}
273
336
 
274
337
  ${e.lede}
275
338
 
@@ -289,4 +352,4 @@ When you are done, call soba_status to confirm it registered.`:S(t)}},{name:"sob
289
352
 
290
353
  SOBA_KEY=${e.key}
291
354
 
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}`);
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,6 @@
1
1
  {
2
2
  "name": "@soba-so/mcp",
3
- "version": "0.0.2",
3
+ "version": "0.0.3",
4
4
  "description": "Sets Soba up from inside your coding agent: it reads the checklist, does the next step, and reads it back.",
5
5
  "license": "UNLICENSED",
6
6
  "homepage": "https://soba.so/docs/quickstart",