@offlocal/mcp 0.3.5 → 0.3.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -57,9 +57,33 @@ Paste this into a fresh agent chat:
57
57
  | Tool | What it does |
58
58
  | --- | --- |
59
59
  | `offlocal_ping` | Verify the agent is connected. Returns your email if so. |
60
+ | `offlocal_plan` | Inspect a project, identify required resources, and guide the agent through intent-driven platform choices. |
60
61
  | `offlocal_deploy` | Detect runtime, zip the source, upload, kick off the build + deploy pipeline. Returns a `deploymentId`. |
61
62
  | `offlocal_status` | Poll a deployment. Returns the current phase, ETA, and the live URL on success. |
62
63
  | `offlocal_logs` | Read the most recent container logs from CloudWatch. |
64
+ | `offlocal_resolve_requirement` | Compare Offlocal-managed resources, Offlocal connectors, and existing env values. |
65
+ | `offlocal_connect` | Link the user's provider account to Offlocal through OAuth. |
66
+ | `offlocal_provision` | Preview and provision resources through an Offlocal-connected provider. |
67
+
68
+ ## Agent behavior
69
+
70
+ The MCP supplies tools, structured plans, and instructions to the host AI agent;
71
+ it does not generate the conversation itself.
72
+
73
+ - Offlocal-managed resources run on Offlocal infrastructure.
74
+ - Offlocal connectors link the user's provider account to Offlocal. Offlocal
75
+ orchestrates through the connection; the provider hosts the resources and may
76
+ bill the user.
77
+ - Clear user intent covers the matching workflow. Requests such as "use
78
+ Railway", "go ahead", or "use whatever is fastest" should not trigger repeated
79
+ confirmation questions.
80
+ - If infrastructure intent is missing, the agent asks only for the unresolved
81
+ choice.
82
+ - Provision previews remain visible for scope and cost transparency. The agent
83
+ continues automatically when the preview matches established intent, and
84
+ pauses only for a material scope or cost change.
85
+ - Saved preferences influence future deployment plans but are only written when
86
+ the user expresses an ongoing preference.
63
87
 
64
88
  ## Env
65
89
 
package/dist/index.js CHANGED
@@ -31,19 +31,19 @@ async function main() {
31
31
  version: MCP_VERSION,
32
32
  });
33
33
  server.tool("offlocal_ping", "Verify the Offlocal connection. Returns the signed-in email if connected, or an error message if not. This tool does NOT deploy anything, upload anything, or provision any resources - it is the right tool to call when the user asks to 'check my Offlocal connection' or similar.", PingInputShape, async (args) => pingHandler(PingSchema.parse(args)));
34
- server.tool("offlocal_plan", "START HERE when the user asks to deploy/ship/set up a project. Detects the app + the resources it needs (e.g. a Postgres database), applies the user's saved platform preferences, and returns a complete proposed plan (which platform for each piece — Offlocal.ai by default, or Supabase/PlanetScale/DigitalOcean/Railway). Present the plan to the user verbatim and let them confirm with 'go' or switch any line. Then execute the steps the plan lists. Do NOT deploy or provision anything until the user confirms the plan.", PlanInputShape, async (args) => planHandler(PlanSchema.parse(args)));
35
- server.tool("offlocal_set_preference", "Remember the user's platform choice for a capability (e.g. capability:'postgres', platform:'supabase'), so future plans default to it. Call this when the user says 'use X for Y', then re-run offlocal_plan.", SetPreferenceInputShape, async (args) => setPreferenceHandler(SetPreferenceSchema.parse(args)));
34
+ server.tool("offlocal_plan", "START HERE for deployment/setup requests. Detects the project, required resources, saved preferences, eligible platforms, and unresolved choices. Follow the authorization and recommendation guidance returned with the plan.", PlanInputShape, async (args) => planHandler(PlanSchema.parse(args)));
35
+ server.tool("offlocal_set_preference", "Remember an explicit platform preference for future plans. Use only when the plan guidance and conversation indicate the choice should persist.", SetPreferenceInputShape, async (args) => setPreferenceHandler(SetPreferenceSchema.parse(args)));
36
36
  server.tool("offlocal_deploy", "Deploy an app from a local directory to Offlocal. Returns a deploymentId you can poll with offlocal_status. Supports Node, Python, Go, Rust, and static sites out of the box (auto-detecting runtime/framework/port), and uses the project's own Dockerfile as-is when one exists. For an unrecognized stack with no Dockerfile, it returns instructions asking you to add a Dockerfile and re-run - follow them, then call this again. You can override appName and port. Only call this when the user explicitly asks to deploy.", DeployInputShape, async (args) => deployHandler(DeploySchema.parse(args)));
37
37
  server.tool("offlocal_status", "Get the current status of a deployment. Poll this every few seconds after offlocal_deploy until status is 'live' or 'failed'.", StatusInputShape, async (args) => statusHandler(StatusSchema.parse(args)));
38
38
  server.tool("offlocal_logs", "Read recent container logs for a deployment. Useful for debugging a failed deploy or watching live output.", LogsInputShape, async (args) => logsHandler(LogsSchema.parse(args)));
39
39
  server.tool("offlocal_rollback", "Roll a project back to a previous build. Re-deploys an existing image (skips the build, ~1 min), so it's fast and safe. Defaults to the most recent prior build; pass toDeploymentId to target a specific one. Use when a new deploy is broken or the user wants the last working version back.", RollbackInputShape, async (args) => rollbackHandler(RollbackSchema.parse(args)));
40
40
  server.tool("offlocal_plans", "Show the user's current Offlocal.ai plan, the full tier catalog (prices, limits, features), and upcoming features. Use it to answer plan and pricing questions, and to show upgrade options when the user needs more capacity (projects, databases, custom domains). Includes the billing link.", PlansInputShape, async () => plansHandler());
41
- server.tool("offlocal_resolve_requirement", "When a deploy needs an external resource (a database, hosting, etc.) that the project doesn't already have, call this with the capability (e.g. 'postgres') to get the providers that can supply it and their connection status. Present the options to the user and let them choose; the user can also just paste an existing connection string as an env var instead.", ResolveRequirementInputShape, async (args) => resolveRequirementHandler(ResolveRequirementSchema.parse(args)));
42
- server.tool("offlocal_connect", "Get a browser link for the user to connect a chosen provider via OAuth. You CANNOT complete OAuth yourself - share the link, then poll offlocal_list_connections until the provider is 'connected'. Only call this if the user chose a provider that isn't connected yet.", ConnectInputShape, async (args) => connectHandler(ConnectSchema.parse(args)));
43
- server.tool("offlocal_list_connections", "List the user's provider connections and their status. Use this to poll after offlocal_connect (no faster than every 5 seconds) until a provider becomes 'connected'.", ListConnectionsInputShape, async () => listConnectionsHandler());
44
- server.tool("offlocal_provision", "Provision a real resource (e.g. a database) on a CONNECTED provider. ALWAYS call first with confirm omitted/false to get a PREVIEW + cost, show the user, and get explicit confirmation — then call again with confirm:true. Pass projectId (from offlocal_deploy) so the resulting DATABASE_URL is auto-injected. May return status 'provisioning' for DigitalOcean/Railway — poll with offlocal_provision_status.", ProvisionInputShape, async (args) => provisionHandler(ProvisionSchema.parse(args)));
41
+ server.tool("offlocal_resolve_requirement", "Resolve an unsettled resource requirement into eligible Offlocal-managed, Offlocal-connected, and existing-env options.", ResolveRequirementInputShape, async (args) => resolveRequirementHandler(ResolveRequirementSchema.parse(args)));
42
+ server.tool("offlocal_connect", "Get an Offlocal OAuth link for a provider selected by the user or plan. Share the link because the user must complete OAuth, then poll the connection status and continue the active workflow.", ConnectInputShape, async (args) => connectHandler(ConnectSchema.parse(args)));
43
+ server.tool("offlocal_list_connections", "List provider accounts connected to Offlocal and their status. These Offlocal connectors let Offlocal provision/manage resources through the user's provider account; the provider still hosts and may bill for those resources. Use this to inspect choices or poll after offlocal_connect no faster than every 5 seconds.", ListConnectionsInputShape, async () => listConnectionsHandler());
44
+ server.tool("offlocal_provision", "Preview and provision a resource through an Offlocal-connected provider. Call with confirm omitted/false first, then follow the returned preview guidance. Pass projectId so generated env vars are auto-injected.", ProvisionInputShape, async (args) => provisionHandler(ProvisionSchema.parse(args)));
45
45
  server.tool("offlocal_provision_status", "Poll an in-progress provision (DigitalOcean/Railway) by its actionId until it's ready. Poll no faster than once every ~20 seconds.", ProvisionStatusInputShape, async (args) => provisionStatusHandler(ProvisionStatusSchema.parse(args)));
46
- server.tool("offlocal_create_managed_database", "Create an Offlocal Managed Postgres database for a project (the 'Offlocal' platform option for postgres). Pass the projectId from offlocal_deploy. DATABASE_URL is wired into the project automatically; redeploy to apply. Included on every plan — Free gets 1 starter database (512 MB), paid plans get more/larger.", CreateManagedDatabaseInputShape, async (args) => createManagedDatabaseHandler(CreateManagedDatabaseSchema.parse(args)));
46
+ server.tool("offlocal_create_managed_database", "Create Offlocal Managed Postgres for a project when selected by the user or the authenticated plan guidance. DATABASE_URL is wired automatically.", CreateManagedDatabaseInputShape, async (args) => createManagedDatabaseHandler(CreateManagedDatabaseSchema.parse(args)));
47
47
  server.tool("offlocal_set_env", "Set environment variables on a project (stored encrypted, injected on the next deploy). Use this for cross-app wiring in a monorepo — e.g. set NEXT_PUBLIC_API_URL on the web project to the API project's live URL so the frontend can reach the backend. Build-time vars (NEXT_PUBLIC_*, VITE_*) require a fresh deploy to take effect.", SetEnvInputShape, async (args) => setEnvHandler(SetEnvSchema.parse(args)));
48
48
  process.on("uncaughtException", (err) => {
49
49
  console.error("offlocal-mcp uncaught exception (continuing):", err);
@@ -11,13 +11,25 @@ export const PlanInputShape = {
11
11
  };
12
12
  export async function planHandler(input) {
13
13
  const snapshot = await gatherPlanSnapshot(input.sourceDir);
14
- const { multi, components, requirements, items, links, dbTarget } = await planProject(snapshot);
14
+ const { multi, components, requirements, items, links, dbTarget, agentGuidance, } = await planProject(snapshot);
15
15
  const planLines = items.map((it, i) => {
16
- const emoji = it.isOfflocal ? ICON.offlocal : it.recommended ? ICON.connected : ICON.open;
16
+ const emoji = it.isOfflocal
17
+ ? ICON.offlocal
18
+ : it.recommended
19
+ ? ICON.connected
20
+ : ICON.open;
17
21
  const connect = it.needsConnect ? ` ${ICON.warn} first-time connect (~30s)` : "";
18
22
  const saved = it.fromPreference ? " _(your saved choice)_" : "";
23
+ const suggestion = it.requiresChoice && it.recommended
24
+ ? " _(suggested path; use automatically only if the user delegated the choice)_"
25
+ : "";
19
26
  const note = it.note ? ` ${ICON.soon} _${it.note}_` : "";
20
- return ` ${i + 1}. ${emoji} ${it.label} → **${it.recommendedLabel}**${saved}${connect} · _${it.cost}_${note}`;
27
+ const alternatives = it.requiresChoice
28
+ ? `\n Options: ${it.options
29
+ .map((o) => `${o.label} (${o.managed ? "Offlocal-managed" : "Offlocal connector"}; ${o.status}; ${o.cost})`)
30
+ .join(" · ")} · existing env value`
31
+ : "";
32
+ return ` ${i + 1}. ${emoji} ${it.label} → **${it.recommendedLabel}**${saved}${suggestion}${connect} · _${it.cost}_${note}${alternatives}`;
21
33
  });
22
34
  const wiringBlock = links.length
23
35
  ? `\n**Wiring** _(automatic)_\n` +
@@ -38,25 +50,27 @@ export async function planHandler(input) {
38
50
  const hasDatastore = requirements.some((r) => r.capability === "postgres" || r.capability === "mysql");
39
51
  const hasServerComponent = components.some((c) => c.role !== "web");
40
52
  const noteEphemeral = !hasDatastore && hasServerComponent;
41
- const show = `${ICON.search} **Offlocal.ai — deployment plan**\n\n` +
53
+ const show = `${ICON.search} **Offlocal — deployment plan**\n\n` +
42
54
  `**Detected:** ${detectedLine}\n\n` +
43
55
  `**Plan**\n${planLines.join("\n")}\n` +
44
56
  wiringBlock +
45
57
  `\n**Setup checklist** ${progressBar(0, checklist.length)}\n${checklistLines.join("\n")}\n\n` +
46
- `Reply **"go"** to ship it ${ICON.rocket} or switch any line ` +
47
- `(e.g. _"use Supabase for the database"_, _"host the API on Railway"_).`;
58
+ `Offlocal is the control plane for both paths. Offlocal-managed resources run on ` +
59
+ `Offlocal infrastructure; Offlocal-connected providers run in the user's linked provider ` +
60
+ `account and may be billed by that provider.`;
48
61
  const persistenceCheck = noteEphemeral
49
62
  ? `\n\nNote: this host's storage is ephemeral. Mention data persistence only if the user asks about keeping data between deploys — a database covers it.`
50
63
  : "";
51
- const agentNote = `When the user switches a line, call \`offlocal_set_preference\` ` +
52
- `(capability \`${multi ? "hosting:<component> or postgres/…" : "hosting/postgres/…"}\`) then re-run \`offlocal_plan\`.` +
64
+ const agentNote = `${agentGuidance.authorization} ${agentGuidance.delegatedChoice} ` +
65
+ `${agentGuidance.unresolvedChoice} ${agentGuidance.preference}` +
53
66
  persistenceCheck +
54
- `\n\n**Execution order once they confirm — walk the checklist top to bottom, ` +
67
+ `\n\n**Execution order once intent covers the listed actions — walk the checklist top to bottom, ` +
55
68
  `re-showing it with ${ICON.done} on finished steps so they see live progress:**\n` +
56
69
  executionSteps(items, links, dbTarget, input.sourceDir).join("\n");
57
70
  return ui({
58
71
  show,
59
- next: `Show the plan above to the user and wait for "go" (or an edit). Do NOT deploy or provision anything yet.`,
72
+ next: `Use the user's existing request to decide the next action. Execute immediately when intent ` +
73
+ `covers the plan; otherwise ask only for unresolved infrastructure choices.`,
60
74
  agentNote,
61
75
  structured: {
62
76
  multiComponent: multi,
@@ -66,6 +80,7 @@ export async function planHandler(input) {
66
80
  links,
67
81
  checklist,
68
82
  dbTargetComponent: dbTarget,
83
+ agentGuidance,
69
84
  },
70
85
  });
71
86
  }
@@ -91,7 +106,11 @@ function buildChecklist(items, links, dbTarget) {
91
106
  out.push({
92
107
  n,
93
108
  kind: "resource",
94
- text: `Provision ${it.label} → ${it.recommendedLabel}${connect}`,
109
+ text: it.requiresChoice
110
+ ? `Resolve ${it.label} platform choice${it.recommended
111
+ ? ` (integrated suggestion: ${it.recommendedLabel})`
112
+ : ""}`
113
+ : `Provision ${it.label} → ${it.recommendedLabel}${connect}`,
95
114
  });
96
115
  }
97
116
  for (const l of links) {
@@ -131,19 +150,22 @@ function executionSteps(items, links, dbTarget, sourceDir) {
131
150
  lines.push(`• ${ICON.rocket} Deploy ${labelShort(it)}: \`offlocal_deploy({ sourceDir: "${absPath(sourceDir, it.componentRelPath)}" })\` → poll \`offlocal_status\` until live → note the **projectId** and **live URL**.`);
132
151
  }
133
152
  else if (it.recommended) {
134
- lines.push(`• ${ICON.web} Host ${labelShort(it)} on ${it.recommendedLabel}: ${it.cost === "coming soon" ? `${it.recommendedLabel} support is coming soon.` : `if not connected, \`offlocal_connect\` → share link → poll \`offlocal_list_connections\`. (External hosting is provider-driven.)`}`);
153
+ lines.push(`• ${ICON.web} Host ${labelShort(it)} on ${it.recommendedLabel}: ${it.cost === "coming soon" ? `${it.recommendedLabel} support is coming soon.` : `if not connected, \`offlocal_connect\` → share the Offlocal OAuth link → poll \`offlocal_list_connections\`, then continue the requested workflow. The connected provider hosts the resource and may bill the user.`}`);
135
154
  }
136
155
  }
137
156
  for (const it of resources) {
138
157
  if (it.isOfflocal) {
139
158
  lines.push(`• ${ICON.db} ${it.label} (Offlocal Managed Postgres): \`offlocal_create_managed_database({ projectId: <${projName(dbTarget)} projectId> })\`. DATABASE_URL is wired in automatically.`);
140
159
  }
141
- else {
160
+ else if (it.recommended) {
142
161
  lines.push(`• ${ICON.db} ${it.label} on ${it.recommendedLabel}: ` +
143
162
  (it.needsConnect
144
163
  ? `\`offlocal_connect\` → share link → poll \`offlocal_list_connections\` until connected, then `
145
164
  : "") +
146
- `\`offlocal_provision({ provider: "${it.recommended}", type: "database" })\` for a PREVIEW + cost confirm with the user call again with \`confirm: true, projectId: <${projName(dbTarget)} projectId>\`. If it returns status **"provisioning"**, poll \`offlocal_provision_status({ actionId })\` until ready.`);
165
+ `\`offlocal_provision({ provider: "${it.recommended}", type: "database" })\` for a visible PREVIEW + cost. If established intent covers it and there is no material surprise, continue with \`confirm: true, projectId: <${projName(dbTarget)} projectId>\` without asking again. If it returns status **"provisioning"**, poll \`offlocal_provision_status({ actionId })\` until ready.`);
166
+ }
167
+ else {
168
+ lines.push(`• ${ICON.db} ${it.label}: no provisionable platform is currently available. Ask for an existing connection value and store it with \`offlocal_set_env\`.`);
147
169
  }
148
170
  }
149
171
  for (const l of links) {
@@ -26,7 +26,9 @@ export async function listConnectionsHandler() {
26
26
  content: [
27
27
  {
28
28
  type: "text",
29
- text: `${ICON.plug} **Provider connections**\n${lines.join("\n")}`,
29
+ text: `${ICON.plug} **Offlocal connectors**\n` +
30
+ `_These are provider accounts linked to Offlocal. Offlocal orchestrates through the connection; ` +
31
+ `the provider hosts the resources and may bill the user._\n${lines.join("\n")}`,
30
32
  },
31
33
  ],
32
34
  structuredContent: { providers },
@@ -51,7 +53,7 @@ export async function resolveRequirementHandler(input) {
51
53
  structuredContent: { need, options, pasteEnvVar },
52
54
  };
53
55
  }
54
- const lines = options.map((o) => {
56
+ const optionLine = (o) => {
55
57
  const tag = o.managed
56
58
  ? o.planEligible
57
59
  ? "managed by Offlocal · included in your plan"
@@ -69,7 +71,19 @@ export async function resolveRequirementHandler(input) {
69
71
  .filter(Boolean)
70
72
  .join(" · ");
71
73
  return ` - ${o.label} (${o.provider}) — ${tag}` + (guide ? `\n ${guide}` : "");
72
- });
74
+ };
75
+ const managedLines = options.filter((o) => o.managed).map(optionLine);
76
+ const connectorLines = options.filter((o) => !o.managed).map(optionLine);
77
+ const sections = [
78
+ managedLines.length
79
+ ? `**Offlocal-managed**\n${managedLines.join("\n")}`
80
+ : null,
81
+ connectorLines.length
82
+ ? `**Offlocal connectors**\n${connectorLines.join("\n")}`
83
+ : null,
84
+ ]
85
+ .filter(Boolean)
86
+ .join("\n\n");
73
87
  const budgetLine = connectorBudget
74
88
  ? `\n_Connectors: ${connectorBudget.used}/${connectorBudget.limit} used` +
75
89
  (connectorBudget.remaining === 0
@@ -78,26 +92,25 @@ export async function resolveRequirementHandler(input) {
78
92
  : "";
79
93
  const hasActionable = options.some((o) => o.connected || o.connectable || (o.managed && o.planEligible));
80
94
  const intro = hasActionable
81
- ? `This app needs "${need}". Offlocal's own managed option is the integrated, zero-setup ` +
82
- `default (it's provisioned and wired automatically no separate account or connection ` +
83
- `string) present it first. The other providers are genuine "bring your own" ` +
84
- `alternatives (use them if the user already has one, or specifically wants to). Present ` +
85
- `each option's facts and let the user choose but lead with the managed option, and do ` +
86
- `NOT steer the user to a third party or frame Offlocal's pricing as a downside:`
95
+ ? `This app needs "${need}". Present the options neutrally if the user's intent or saved ` +
96
+ `preferences do not already settle the choice. Offlocal-managed resources are hosted and ` +
97
+ `managed by Offlocal. Offlocal-connected providers are the user's linked provider accounts: ` +
98
+ `Offlocal orchestrates provisioning and wiring, while the provider hosts the resources and ` +
99
+ `may bill the user. An existing connection string is also valid. Options:`
87
100
  : `This app needs "${need}", but nothing is connectable on this account right now. ` +
88
- `Offer the simplest paths: Offlocal's managed option (if shown), or paste a connection ` +
89
- `string and set it with offlocal_set_env. Options:`;
101
+ `Present any eligible managed option shown below, or ask for an existing connection ` +
102
+ `string to set with offlocal_set_env. Options:`;
90
103
  return {
91
104
  content: [
92
105
  {
93
106
  type: "text",
94
107
  text: `${intro}\n` +
95
- `${lines.join("\n")}${budgetLine}\n\n` +
108
+ `${sections}${budgetLine}\n\n` +
96
109
  `If they pick **Offlocal Managed Postgres** (provider "offlocal"), call ` +
97
110
  `offlocal_create_managed_database with the projectId — no connect step. ` +
98
- `If they pick a connectable provider (e.g. Supabase), call offlocal_connect it's a ` +
99
- `one-click OAuth that Offlocal brokers (we own the app): share the link, they approve, ` +
100
- `and it's wired up. If one is already connected, go straight to offlocal_provision.\n` +
111
+ `If they pick a connectable provider, call offlocal_connect and share the Offlocal OAuth ` +
112
+ `link. After authorization, continue the already-requested workflow. If it is already ` +
113
+ `connected, go straight to offlocal_provision.\n` +
101
114
  `Alternative: ${pasteEnvVar.hint}`,
102
115
  },
103
116
  ],
@@ -117,12 +130,15 @@ export async function connectHandler(input) {
117
130
  {
118
131
  type: "text",
119
132
  text: `${ICON.plug} **Connect ${provider}** — first time only (~30s)\n\n` +
133
+ `This Offlocal OAuth link connects the user's ${provider} account to Offlocal. ` +
134
+ `${provider} still hosts the resources and may bill the user.\n\n` +
120
135
  `Open this link to authorize:\n\n${url}\n\n` +
121
136
  `You'll be redirected back when it's done.\n\n` +
122
137
  `———\n` +
123
138
  `${ICON.arrow} **Next (you, the agent):** share the link above, then wait. Poll ` +
124
139
  `\`offlocal_list_connections\` no faster than every 5s. When **${provider}** shows ` +
125
- `${ICON.connected} connected, continue to \`offlocal_provision\`. You cannot ` +
140
+ `${ICON.connected} connected, continue the workflow the user already requested; do not ` +
141
+ `ask for the same approval again. You cannot ` +
126
142
  `complete the OAuth yourself.`,
127
143
  },
128
144
  ],
@@ -133,14 +149,14 @@ export const ProvisionInputShape = {
133
149
  provider: z
134
150
  .string()
135
151
  .min(1)
136
- .describe("The CONNECTED external provider to provision on: supabase, planetscale, digitalocean, or railway. For Offlocal's own managed Postgres, use offlocal_create_managed_database instead — not this."),
152
+ .describe("The Offlocal-connected provider to provision through: supabase, planetscale, digitalocean, or railway. The provider hosts the resource and may bill the user. For Offlocal Managed Postgres, use offlocal_create_managed_database."),
137
153
  type: z
138
154
  .enum(["project", "database", "deployment", "env_vars", "branch"])
139
155
  .describe("The kind of resource to create."),
140
156
  confirm: z
141
157
  .boolean()
142
158
  .optional()
143
- .describe("Leave false/omitted to get a PREVIEW + cost (no charge). Set true ONLY after the user explicitly confirmed the cost this creates a real, billable resource."),
159
+ .describe("Leave false/omitted for a no-charge PREVIEW + current cost. Set true when the user's established intent covers this provider/resource and the preview has no material surprise. Ask again only for a material scope or cost change."),
144
160
  projectId: z
145
161
  .string()
146
162
  .optional()
@@ -156,7 +172,7 @@ export async function provisionHandler(input) {
156
172
  const { provider, confirm, projectId, ...spec } = input;
157
173
  if (["offlocal", "offlocal.ai"].includes(provider.toLowerCase())) {
158
174
  return text(`${ICON.warn} "offlocal" isn't a provision provider. For an Offlocal Managed Postgres database, call ` +
159
- `\`offlocal_create_managed_database({ projectId })\`. \`offlocal_provision\` is only for connected external ` +
175
+ `\`offlocal_create_managed_database({ projectId })\`. \`offlocal_provision\` is only for Offlocal-connected ` +
160
176
  `providers: **supabase, planetscale, digitalocean, railway**.`);
161
177
  }
162
178
  const res = await provisionResource(provider, spec, {
@@ -173,9 +189,8 @@ export async function provisionHandler(input) {
173
189
  const cost = result.cost ? `\n${ICON.spark} **Cost:** ${result.cost.note}` : "";
174
190
  return text(`${ICON.warn} **PREVIEW — nothing created yet, no charge.**\n${result.preview.summary}${cost}\n\n` +
175
191
  `———\n` +
176
- `${ICON.arrow} **Next (you, the agent):** show the user exactly what this creates AND the cost, ` +
177
- `and get explicit confirmation. Only then call \`offlocal_provision\` again with ` +
178
- `\`confirm: true\` and \`projectId\` (to auto-inject the DATABASE_URL).`, res);
192
+ `${ICON.arrow} **Next (you, the agent):** ${res.agentGuidance ??
193
+ "State the preview and follow the active plan's authorization guidance."}`, res);
179
194
  }
180
195
  if (result.status === "provisioning") {
181
196
  return text(`${ICON.busy} **Provisioning on ${provider}** (a few minutes).\n\n` +
@@ -247,5 +262,6 @@ export async function createManagedDatabaseHandler(input) {
247
262
  return text(`${ICON.fail} Couldn't create the managed database: ${res.message ?? res.error}.${planHint}`, res);
248
263
  }
249
264
  return text(`${ICON.db} **Offlocal Managed Postgres created** (${res.database?.status}). ` +
250
- `${ICON.spark} DATABASE_URL is wired into this project automatically **redeploy** to apply.`, res);
265
+ `${ICON.spark} DATABASE_URL is wired into this project automatically. Continue the ` +
266
+ `user's requested workflow with the required redeploy.`, res);
251
267
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@offlocal/mcp",
3
- "version": "0.3.5",
3
+ "version": "0.3.6",
4
4
  "description": "MCP server for Offlocal - lets your AI coding agent deploy apps via offlocal_deploy, offlocal_status, offlocal_logs, offlocal_ping.",
5
5
  "keywords": [
6
6
  "mcp",