ashlr-stack-mcp 0.2.2 → 0.3.1

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 +26 -7
  2. package/dist/server.js +121 -31
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -1,10 +1,10 @@
1
1
  # ashlr-stack-mcp
2
2
 
3
- > MCP server for [Ashlr Stack](https://stack.ashlr.ai). Exposes 19 tools and up to 3 resources so Claude Code / Cursor / Windsurf / Codex can provision every third-party service in a project with one call.
3
+ > MCP server for [Ashlr Stack](https://stack.ashlr.ai). Exposes 19 tools and up to 3 resources for inspecting and configuring supported services from MCP clients.
4
4
 
5
5
  ## What it does
6
6
 
7
- `stack` is a CLI. `ashlr-stack-mcp` is the same capabilities as MCP tools so your coding agent can drive Stack directly. Ask Claude "add Supabase to this project" and it calls `stack_add`, which walks OAuth, provisions the project, writes secrets through Phantom, and updates `.mcp.json` — all without leaving the chat.
7
+ `stack` is a CLI. `ashlr-stack-mcp` is the same capabilities as MCP tools so your coding agent can drive Stack directly. An agent can request `stack_add` for a supported provider; authentication, resource setup and configuration changes depend on the adapter and authorization. Inspect the result and any changes.
8
8
 
9
9
  ## Install
10
10
 
@@ -26,7 +26,25 @@ bun add -g ashlr-stack-mcp @ashlr/stack
26
26
  npm i -g ashlr-stack-mcp @ashlr/stack
27
27
  ```
28
28
 
29
- Every tool runs the `stack` CLI, so install `@ashlr/stack` alongside this package (or set `STACK_BIN` to the CLI). Without it the server starts and lists its tools, but each call returns "Could not run the 'stack' CLI" with the fix. The npm 0.1.1 release runs on Bun, so Bun must be installed. In source 0.2.1 this package depends on `@ashlr/stack` and runs that dependency's `stack` bin; the order is `STACK_BIN`, then that bin, then `stack` on PATH.
29
+ Every tool runs the `stack` CLI. The server resolves it in this order: the executable specified by `STACK_BIN`, the installed `@ashlr/stack` dependency's bin, then `stack` on PATH. If none is available, the server can still start and list tools, but calls fail with a CLI error. The npm CLI and MCP packages in this release declare Node.js >= 20 and use Node entrypoints. Check your installed package manifests; source development uses Bun.
30
+
31
+ ## Run from a local clone
32
+
33
+ Install Bun and Node.js 20 or later, then install the workspace dependencies and build the CLI from the repository root:
34
+
35
+ ```bash
36
+ bun install --frozen-lockfile
37
+ (cd packages/cli && bun run build)
38
+ ```
39
+
40
+ The source server runs with Bun; the CLI build provides the workspace dependency's `dist/index.js`. Start the server from the project you want it to inspect, replacing the paths with absolute paths on your machine:
41
+
42
+ ```bash
43
+ cd /absolute/path/to/my-project
44
+ bun run /absolute/path/to/ashlr-stack/packages/mcp/src/server.ts
45
+ ```
46
+
47
+ The server speaks MCP over stdin and stdout, so connect it to an MCP client rather than expecting an interactive prompt. Keep stdout reserved for the protocol. Provider operations may need Phantom and provider credentials configured separately.
30
48
 
31
49
  ## Wire it into your agent
32
50
 
@@ -42,23 +60,24 @@ Drop this into your project's `.mcp.json` (Claude Code, Cursor and Windsurf read
42
60
  }
43
61
  ```
44
62
 
45
- If you installed from source (no global bin), point at the entry directly:
63
+ For a local clone, after the installation and CLI build above, point the client at the source entry directly (with `bun` available on its PATH):
46
64
 
47
65
  ```json
48
66
  {
49
67
  "mcpServers": {
50
68
  "ashlr-stack": {
51
69
  "command": "bun",
52
- "args": ["run", "/absolute/path/to/ashlr-stack/packages/mcp/src/server.ts"],
53
- "env": { "STACK_BIN": "/absolute/path/to/ashlr-stack/packages/cli/src/index.ts" }
70
+ "args": ["run", "/absolute/path/to/ashlr-stack/packages/mcp/src/server.ts"]
54
71
  }
55
72
  }
56
73
  }
57
74
  ```
58
75
 
76
+ The client chooses the server's working directory; the absolute entry path does not set it. Configure the client to start the server in your project if supported, or pass `cwd` to project-scoped tool calls as described below.
77
+
59
78
  ## Targeting a project (`cwd`)
60
79
 
61
- Every tool runs `stack` in the server's working directory unless you pass `cwd`. When your client starts the server outside the project (Claude Desktop, a global Cursor or Windsurf config), pass the project's absolute path:
80
+ Every tool runs `stack` in the server's working directory by default. When your client starts the server outside the project (Claude Desktop, a global Cursor or Windsurf config), pass the project's absolute path to a project-scoped tool:
62
81
 
63
82
  ```json
64
83
  { "name": "stack_list", "arguments": { "json": true, "cwd": "/home/me/code/my-app" } }
package/dist/server.js CHANGED
@@ -6508,7 +6508,7 @@ var require_dist = __commonJS(function(exports, module) {
6508
6508
 
6509
6509
  // src/server.ts
6510
6510
  import { spawn } from "child_process";
6511
- import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
6511
+ import { existsSync, readFileSync as readFileSync2 } from "fs";
6512
6512
  import { join } from "path";
6513
6513
 
6514
6514
  // ../../node_modules/.bun/zod@4.3.6/node_modules/zod/v4/core/core.js
@@ -14197,8 +14197,8 @@ var ServerResultSchema2 = union([
14197
14197
  ]);
14198
14198
 
14199
14199
  // src/cwd.ts
14200
- import { realpathSync, statSync } from "node:fs";
14201
- import { isAbsolute } from "node:path";
14200
+ import { lstatSync, realpathSync, statSync } from "node:fs";
14201
+ import { dirname, isAbsolute, relative, resolve, sep } from "node:path";
14202
14202
 
14203
14203
  // src/tools.ts
14204
14204
  function validateToolInput(tool, input) {
@@ -14274,13 +14274,97 @@ function planToolCall(tool, input) {
14274
14274
  const resolved = resolveToolCwd(tool, input);
14275
14275
  if (resolved.error !== undefined)
14276
14276
  return { ok: false, error: resolved.error };
14277
+ const pathKey = tool.name === "stack_scan" ? "path" : tool.name === "stack_import" ? "from" : null;
14278
+ const project = resolved.cwd ?? process.cwd();
14279
+ if (pathKey) {
14280
+ const value = input[pathKey] ?? (pathKey === "path" ? "." : ".env");
14281
+ const error = validateProjectPath(tool, pathKey, String(value), project);
14282
+ if (error)
14283
+ return { ok: false, error };
14284
+ }
14285
+ if (tool.name === "stack_apply" && typeof input.recipe_id === "string") {
14286
+ if (!/^[a-zA-Z0-9_-]+$/.test(input.recipe_id)) {
14287
+ return {
14288
+ ok: false,
14289
+ error: `${tool.name}: argument "recipe_id" must be a filename stem using letters, numbers, underscores or hyphens.`
14290
+ };
14291
+ }
14292
+ const error = validateProjectPath(tool, "recipe_id", `.stack/recipes/${input.recipe_id}.toml`, project);
14293
+ if (error)
14294
+ return { ok: false, error };
14295
+ }
14277
14296
  return { ok: true, args: tool.cliArgs(input), cwd: resolved.cwd };
14278
14297
  }
14298
+ function canonicalPath(path) {
14299
+ try {
14300
+ return realpathSync(path);
14301
+ } catch (error) {
14302
+ if (error.code !== "ENOENT")
14303
+ throw error;
14304
+ try {
14305
+ if (lstatSync(path).isSymbolicLink())
14306
+ throw error;
14307
+ } catch (statError) {
14308
+ if (statError === error || statError.code !== "ENOENT") {
14309
+ throw statError;
14310
+ }
14311
+ }
14312
+ const parent = dirname(path);
14313
+ if (parent === path)
14314
+ throw error;
14315
+ return resolve(canonicalPath(parent), relative(parent, path));
14316
+ }
14317
+ }
14318
+ function validateProjectPath(tool, key, value, project) {
14319
+ try {
14320
+ const root = realpathSync(project);
14321
+ const target = canonicalPath(isAbsolute(value) ? value : `${root}${sep}${value}`);
14322
+ const suffix = relative(root, target);
14323
+ if (suffix !== ".." && !suffix.startsWith(`..${sep}`) && !isAbsolute(suffix))
14324
+ return null;
14325
+ } catch {
14326
+ return `${tool.name}: argument "${key}" cannot be resolved; use a valid path inside the project directory.`;
14327
+ }
14328
+ return `${tool.name}: argument "${key}" must stay inside the project directory (got ${JSON.stringify(value)}); use a path relative to cwd within the project.`;
14329
+ }
14330
+
14331
+ // src/output.ts
14332
+ function compactJsonOutput(stdout, args) {
14333
+ const text = stdout.trim();
14334
+ if (!args.includes("--json"))
14335
+ return text;
14336
+ try {
14337
+ JSON.parse(text);
14338
+ } catch {
14339
+ return text;
14340
+ }
14341
+ let quoted = false;
14342
+ let escaped = false;
14343
+ let result = "";
14344
+ for (const char of text) {
14345
+ if (quoted) {
14346
+ result += char;
14347
+ if (escaped)
14348
+ escaped = false;
14349
+ else if (char === "\\")
14350
+ escaped = true;
14351
+ else if (char === '"')
14352
+ quoted = false;
14353
+ } else if (char === '"') {
14354
+ quoted = true;
14355
+ result += char;
14356
+ } else if (!`
14357
+ \r `.includes(char)) {
14358
+ result += char;
14359
+ }
14360
+ }
14361
+ return result;
14362
+ }
14279
14363
 
14280
14364
  // src/stack-bin.ts
14281
- import { existsSync, readFileSync } from "node:fs";
14365
+ import { readFileSync, statSync as statSync2 } from "node:fs";
14282
14366
  import { createRequire } from "node:module";
14283
- import { dirname, resolve } from "node:path";
14367
+ import { dirname as dirname2, resolve as resolve2 } from "node:path";
14284
14368
  import { fileURLToPath } from "node:url";
14285
14369
  var CLI_PACKAGE_JSON = ["@ashlr", "stack", "package.json"].join("/");
14286
14370
  function resolveCliPackageJson() {
@@ -14296,8 +14380,8 @@ function cliBinFromPackageJson(pkgJsonPath) {
14296
14380
  const rel = typeof pkg.bin === "string" ? pkg.bin : pkg.bin?.stack;
14297
14381
  if (!rel)
14298
14382
  return;
14299
- const abs = resolve(dirname(pkgJsonPath), rel);
14300
- return existsSync(abs) ? abs : undefined;
14383
+ const abs = resolve2(dirname2(pkgJsonPath), rel);
14384
+ return statSync2(abs).isFile() ? abs : undefined;
14301
14385
  } catch {
14302
14386
  return;
14303
14387
  }
@@ -14314,7 +14398,7 @@ function resolveStackCommand(env = process.env, findPackageJson = resolveCliPack
14314
14398
  }
14315
14399
  function readServerVersion(moduleUrl = import.meta.url) {
14316
14400
  try {
14317
- const pkgPath = resolve(dirname(fileURLToPath(moduleUrl)), "..", "package.json");
14401
+ const pkgPath = resolve2(dirname2(fileURLToPath(moduleUrl)), "..", "package.json");
14318
14402
  const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
14319
14403
  if (pkg.name === "ashlr-stack-mcp" && pkg.version)
14320
14404
  return pkg.version;
@@ -14325,7 +14409,7 @@ function readServerVersion(moduleUrl = import.meta.url) {
14325
14409
  // src/tools.ts
14326
14410
  var CWD_INPUT = {
14327
14411
  type: "string",
14328
- description: "Absolute path of the project directory to run in (default: the MCP server's working directory). Must be an existing directory."
14412
+ description: "Existing absolute project directory; defaults to server cwd."
14329
14413
  };
14330
14414
  var TOOLS = [
14331
14415
  {
@@ -14334,7 +14418,7 @@ var TOOLS = [
14334
14418
  inputSchema: {
14335
14419
  type: "object",
14336
14420
  properties: {
14337
- template: { type: "string", description: "Optional starter template name." },
14421
+ template: { type: "string", description: "Starter template name." },
14338
14422
  force: { type: "boolean", description: "Overwrite an existing .stack.toml." },
14339
14423
  cwd: CWD_INPUT
14340
14424
  }
@@ -14354,7 +14438,10 @@ var TOOLS = [
14354
14438
  inputSchema: {
14355
14439
  type: "object",
14356
14440
  properties: {
14357
- from: { type: "string", description: "Path to the .env file (default: .env)." },
14441
+ from: {
14442
+ type: "string",
14443
+ description: "Path to the .env file inside the project (default: .env)."
14444
+ },
14358
14445
  dryRun: { type: "boolean", description: "Preview without writing." },
14359
14446
  cwd: CWD_INPUT
14360
14447
  }
@@ -14374,7 +14461,10 @@ var TOOLS = [
14374
14461
  inputSchema: {
14375
14462
  type: "object",
14376
14463
  properties: {
14377
- path: { type: "string", description: "Directory to scan (default: cwd)." },
14464
+ path: {
14465
+ type: "string",
14466
+ description: "Directory to scan inside the project (default: cwd)."
14467
+ },
14378
14468
  confidence: {
14379
14469
  type: "string",
14380
14470
  description: "Minimum confidence: low, medium, or high (default: medium)."
@@ -14437,7 +14527,7 @@ var TOOLS = [
14437
14527
  properties: {
14438
14528
  json: {
14439
14529
  type: "boolean",
14440
- description: "Machine-readable JSON (schema ashlr.stack.list/v1)."
14530
+ description: "JSON: ashlr.stack.list/v1."
14441
14531
  },
14442
14532
  cwd: CWD_INPUT
14443
14533
  }
@@ -14453,7 +14543,7 @@ var TOOLS = [
14453
14543
  service: { type: "string" },
14454
14544
  json: {
14455
14545
  type: "boolean",
14456
- description: "Machine-readable JSON (schema ashlr.stack.info/v1)."
14546
+ description: "JSON: ashlr.stack.info/v1."
14457
14547
  },
14458
14548
  cwd: CWD_INPUT
14459
14549
  },
@@ -14474,7 +14564,7 @@ var TOOLS = [
14474
14564
  properties: {
14475
14565
  json: {
14476
14566
  type: "boolean",
14477
- description: "Machine-readable JSON (schema ashlr.stack.status/v1)."
14567
+ description: "JSON: ashlr.stack.status/v1."
14478
14568
  },
14479
14569
  cwd: CWD_INPUT
14480
14570
  }
@@ -14489,7 +14579,7 @@ var TOOLS = [
14489
14579
  properties: {
14490
14580
  json: {
14491
14581
  type: "boolean",
14492
- description: "Machine-readable JSON (schema ashlr.stack.env/v1, names only)."
14582
+ description: "JSON: ashlr.stack.env/v1; names only."
14493
14583
  },
14494
14584
  cwd: CWD_INPUT
14495
14585
  }
@@ -14504,7 +14594,7 @@ var TOOLS = [
14504
14594
  properties: {
14505
14595
  json: {
14506
14596
  type: "boolean",
14507
- description: "Machine-readable JSON (schema ashlr.stack.env-diff/v1, names and declaring service only); still an error result when any are missing."
14597
+ description: "JSON: ashlr.stack.env-diff/v1; names and service only. Missing secrets remain an error."
14508
14598
  },
14509
14599
  cwd: CWD_INPUT
14510
14600
  }
@@ -14519,7 +14609,7 @@ var TOOLS = [
14519
14609
  properties: {
14520
14610
  fix: { type: "boolean", description: "Attempt auto-remediation." },
14521
14611
  all: { type: "boolean", description: "Run across every registered project." },
14522
- json: { type: "boolean", description: "Machine-readable JSON output." },
14612
+ json: { type: "boolean", description: "JSON output." },
14523
14613
  cwd: CWD_INPUT
14524
14614
  }
14525
14615
  },
@@ -14559,7 +14649,7 @@ var TOOLS = [
14559
14649
  },
14560
14650
  json: {
14561
14651
  type: "boolean",
14562
- description: "Machine-readable JSON (schema ashlr.stack.providers/v1)."
14652
+ description: "JSON: ashlr.stack.providers/v1."
14563
14653
  }
14564
14654
  }
14565
14655
  },
@@ -14577,7 +14667,7 @@ var TOOLS = [
14577
14667
  properties: {
14578
14668
  json: {
14579
14669
  type: "boolean",
14580
- description: "Machine-readable JSON (schema ashlr.stack.projects/v1)."
14670
+ description: "JSON: ashlr.stack.projects/v1."
14581
14671
  }
14582
14672
  }
14583
14673
  },
@@ -14591,7 +14681,7 @@ var TOOLS = [
14591
14681
  properties: {
14592
14682
  json: {
14593
14683
  type: "boolean",
14594
- description: "Machine-readable JSON (schema ashlr.stack.deps/v1)."
14684
+ description: "JSON: ashlr.stack.deps/v1."
14595
14685
  },
14596
14686
  cwd: CWD_INPUT
14597
14687
  }
@@ -14606,7 +14696,7 @@ var TOOLS = [
14606
14696
  properties: {
14607
14697
  json: {
14608
14698
  type: "boolean",
14609
- description: "Machine-readable JSON (schema ashlr.stack.templates/v1): each template with its services, providers and secret names."
14699
+ description: "JSON: ashlr.stack.templates/v1; services, providers and secret names."
14610
14700
  }
14611
14701
  }
14612
14702
  },
@@ -14620,7 +14710,7 @@ var TOOLS = [
14620
14710
  },
14621
14711
  {
14622
14712
  name: "stack_recommend",
14623
- description: "Retrieve scored curated-provider matches and per-category ranking for a query. save:true writes a Recipe for a later stack_apply request. The CLI JSON payload is forwarded as text; this tool does not request LLM inference.",
14713
+ description: "Rank curated providers by query, returning JSON as text without LLM inference. save:true writes a Recipe for stack_apply.",
14624
14714
  inputSchema: {
14625
14715
  type: "object",
14626
14716
  properties: {
@@ -14634,11 +14724,11 @@ var TOOLS = [
14634
14724
  },
14635
14725
  category: {
14636
14726
  type: "string",
14637
- description: "Optional filter to a source catalog category: Database, Deploy, Cloud, AI, Analytics, Observability, Errors, Payments, Code, Tickets, Email, Auth, FeatureFlags. Matched case-insensitively; an unknown category is an error."
14727
+ description: "Category (case-insensitive): Database, Deploy, Cloud, AI, Analytics, Observability, Errors, Payments, Code, Tickets, Email, Auth, FeatureFlags. Unknown values error."
14638
14728
  },
14639
14729
  save: {
14640
14730
  type: "boolean",
14641
- description: "When true, also freeze the result to .stack/recipes/<id>.toml so you can run stack_apply. Returns the recipe id in the response."
14731
+ description: "Save .stack/recipes/<id>.toml for stack_apply; returns recipe id."
14642
14732
  },
14643
14733
  cwd: CWD_INPUT
14644
14734
  },
@@ -14663,7 +14753,7 @@ var TOOLS = [
14663
14753
  properties: {
14664
14754
  recipe_id: {
14665
14755
  type: "string",
14666
- description: "Recipe id (filename stem in .stack/recipes/)."
14756
+ description: "Recipe id (filename stem in .stack/recipes/, letters, numbers, underscores or hyphens)."
14667
14757
  },
14668
14758
  no_wire: {
14669
14759
  type: "boolean",
@@ -14726,7 +14816,7 @@ server.setRequestHandler(CallToolRequestSchema2, async (request) => {
14726
14816
  }
14727
14817
  const { args, cwd } = plan;
14728
14818
  const { stdout, stderr, code } = await runStack(args, cwd);
14729
- const body = stdout.trim() + (stderr.trim() ? `
14819
+ const body = compactJsonOutput(stdout, args) + (stderr.trim() ? `
14730
14820
 
14731
14821
  [stderr]
14732
14822
  ${stderr.trim()}` : "");
@@ -14738,7 +14828,7 @@ ${stderr.trim()}` : "");
14738
14828
  server.setRequestHandler(ListResourcesRequestSchema2, async () => {
14739
14829
  const resources = [];
14740
14830
  const cwd = process.cwd();
14741
- if (existsSync2(join(cwd, ".stack.toml"))) {
14831
+ if (existsSync(join(cwd, ".stack.toml"))) {
14742
14832
  resources.push({
14743
14833
  uri: "stack://current/.stack.toml",
14744
14834
  name: ".stack.toml (this project)",
@@ -14746,7 +14836,7 @@ server.setRequestHandler(ListResourcesRequestSchema2, async () => {
14746
14836
  mimeType: "application/toml"
14747
14837
  });
14748
14838
  }
14749
- if (existsSync2(join(cwd, ".stack.local.toml"))) {
14839
+ if (existsSync(join(cwd, ".stack.local.toml"))) {
14750
14840
  resources.push({
14751
14841
  uri: "stack://current/.stack.local.toml",
14752
14842
  name: ".stack.local.toml (local instance)",
@@ -14754,7 +14844,7 @@ server.setRequestHandler(ListResourcesRequestSchema2, async () => {
14754
14844
  mimeType: "application/toml"
14755
14845
  });
14756
14846
  }
14757
- if (existsSync2(join(cwd, ".mcp.json"))) {
14847
+ if (existsSync(join(cwd, ".mcp.json"))) {
14758
14848
  resources.push({
14759
14849
  uri: "stack://current/.mcp.json",
14760
14850
  name: ".mcp.json (MCP wiring)",
@@ -14775,7 +14865,7 @@ server.setRequestHandler(ReadResourceRequestSchema2, async (request) => {
14775
14865
  throw new Error(`Refusing to read: ${rel}`);
14776
14866
  }
14777
14867
  const path = join(process.cwd(), rel);
14778
- if (!existsSync2(path))
14868
+ if (!existsSync(path))
14779
14869
  throw new Error(`${rel} does not exist in cwd.`);
14780
14870
  const text = readFileSync2(path, "utf-8");
14781
14871
  return {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ashlr-stack-mcp",
3
- "version": "0.2.2",
3
+ "version": "0.3.1",
4
4
  "mcpName": "io.github.ashlrai/ashlr-stack",
5
5
  "description": "MCP server for Ashlr Stack. Exposes 19 tools and 3 resources so Claude Code / Cursor / Windsurf / Codex can provision every third-party service in a project with one call.",
6
6
  "license": "MIT",
@@ -51,7 +51,7 @@
51
51
  "typecheck": "bunx tsc --noEmit"
52
52
  },
53
53
  "dependencies": {
54
- "@ashlr/stack": "^0.2.2",
54
+ "@ashlr/stack": "^0.3.1",
55
55
  "@modelcontextprotocol/sdk": "^1.0.0"
56
56
  },
57
57
  "devDependencies": {