From 930a2b70d885d618ecb48e540321cf83fa4ffbf8 Mon Sep 17 00:00:00 2001 From: Mathew Goldsborough <1759329+mgoldsborough@users.noreply.github.com> Date: Sat, 28 Mar 2026 01:10:11 -1000 Subject: [PATCH 1/3] Set UPJACK_ROOT for Upjack bundles during mpak run When running an Upjack app (manifest has _meta["ai.nimblebrain/upjack"]), set UPJACK_ROOT to MPAK_WORKSPACE so entity data persists outside the ephemeral bundle cache. Non-Upjack bundles are unaffected. --- packages/cli/src/commands/packages/run.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/packages/cli/src/commands/packages/run.ts b/packages/cli/src/commands/packages/run.ts index 17d6b5e..b4c5919 100644 --- a/packages/cli/src/commands/packages/run.ts +++ b/packages/cli/src/commands/packages/run.ts @@ -524,6 +524,15 @@ export async function handleRun( // Defaults to $CWD/.mpak — user can override via MPAK_WORKSPACE in their environment. env["MPAK_WORKSPACE"] = resolveWorkspace(env["MPAK_WORKSPACE"], process.cwd()); + // If the bundle is an Upjack app, set UPJACK_ROOT so entity data persists + // outside the bundle cache. + const manifestAny = manifest as unknown as Record; + const upjackMeta = manifestAny["_meta"] as Record | undefined; + const upjackExt = upjackMeta?.["ai.nimblebrain/upjack"] as Record | undefined; + if (upjackExt?.["namespace"]) { + env["UPJACK_ROOT"] = env["MPAK_WORKSPACE"]!; + } + // Spawn with stdio passthrough for MCP const child = spawn(command, args, { stdio: ["inherit", "inherit", "inherit"], From 598b1651e3fb6747094cceccf84767d5cee61fbe Mon Sep 17 00:00:00 2001 From: Mathew Goldsborough <1759329+mgoldsborough@users.noreply.github.com> Date: Sat, 28 Mar 2026 15:27:36 -1000 Subject: [PATCH 2/3] Replace Upjack-specific env logic with generic ${mpak.*} variable substitution MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bundles can now reference mpak runtime values (workspace, cache_dir, bundle_name) in mcp_config.env via ${mpak.*} placeholders. This keeps mpak framework-agnostic — the Upjack bundle declares its own env needs in its manifest instead of mpak knowing about Upjack. --- .../cli/src/commands/packages/run.test.ts | 76 +++++++++++++++++++ packages/cli/src/commands/packages/run.ts | 53 +++++++++---- 2 files changed, 114 insertions(+), 15 deletions(-) diff --git a/packages/cli/src/commands/packages/run.test.ts b/packages/cli/src/commands/packages/run.test.ts index 6038059..184f913 100644 --- a/packages/cli/src/commands/packages/run.test.ts +++ b/packages/cli/src/commands/packages/run.test.ts @@ -6,6 +6,7 @@ import { resolveArgs, resolveWorkspace, substituteUserConfig, + substituteMpakVars, substituteEnvVars, getLocalCacheDir, localBundleNeedsExtract, @@ -261,6 +262,46 @@ describe("substituteEnvVars", () => { DEBUG: "true", }); }); + + it("substitutes mpak runtime vars when provided", () => { + const env = { + DATA_DIR: "${mpak.workspace}/data", + CACHE: "${mpak.cache_dir}", + NAME: "${mpak.bundle_name}", + }; + const mpakVars = { + workspace: "/home/.mpak", + cache_dir: "/home/.mpak/cache/test", + bundle_name: "@scope/test", + }; + expect(substituteEnvVars(env, {}, mpakVars)).toEqual({ + DATA_DIR: "/home/.mpak/data", + CACHE: "/home/.mpak/cache/test", + NAME: "@scope/test", + }); + }); + + it("substitutes both user_config and mpak vars in the same value", () => { + const env = { + DSN: "${user_config.db_host}:${mpak.workspace}/db", + }; + expect( + substituteEnvVars( + env, + { db_host: "localhost" }, + { workspace: "/data" }, + ), + ).toEqual({ + DSN: "localhost:/data/db", + }); + }); + + it("leaves unmatched mpak vars intact", () => { + const env = { X: "${mpak.unknown}" }; + expect(substituteEnvVars(env, {}, { workspace: "/w" })).toEqual({ + X: "${mpak.unknown}", + }); + }); }); describe("getLocalCacheDir", () => { @@ -333,3 +374,38 @@ describe("resolveWorkspace", () => { ); }); }); + +describe("substituteMpakVars", () => { + it("replaces ${mpak.workspace}", () => { + expect( + substituteMpakVars("${mpak.workspace}/data", { workspace: "/home/.mpak" }), + ).toBe("/home/.mpak/data"); + }); + + it("replaces multiple mpak vars in one string", () => { + expect( + substituteMpakVars("${mpak.workspace}/${mpak.bundle_name}", { + workspace: "/w", + bundle_name: "@scope/test", + }), + ).toBe("/w/@scope/test"); + }); + + it("leaves unmatched mpak vars intact", () => { + expect( + substituteMpakVars("${mpak.missing}/path", { workspace: "/w" }), + ).toBe("${mpak.missing}/path"); + }); + + it("leaves non-mpak placeholders untouched", () => { + expect( + substituteMpakVars("${user_config.key}", { workspace: "/w" }), + ).toBe("${user_config.key}"); + }); + + it("handles string with no placeholders", () => { + expect( + substituteMpakVars("plain-value", { workspace: "/w" }), + ).toBe("plain-value"); + }); +}); diff --git a/packages/cli/src/commands/packages/run.ts b/packages/cli/src/commands/packages/run.ts index b4c5919..cd243d7 100644 --- a/packages/cli/src/commands/packages/run.ts +++ b/packages/cli/src/commands/packages/run.ts @@ -117,6 +117,23 @@ export function resolveWorkspace( return override || join(cwd, ".mpak"); } +/** + * Substitute ${mpak.*} runtime variable placeholders in a string. + * Available variables: workspace, cache_dir, bundle_name + * @example substituteMpakVars('${mpak.workspace}/data', { workspace: '/home/.mpak' }) => '/home/.mpak/data' + */ +export function substituteMpakVars( + value: string, + vars: Record, +): string { + return value.replace( + /\$\{mpak\.([^}]+)\}/g, + (match, key: string) => { + return vars[key] ?? match; + }, + ); +} + /** * Substitute ${user_config.*} placeholders in a string * @example substituteUserConfig('${user_config.api_key}', { api_key: 'secret' }) => 'secret' @@ -134,16 +151,21 @@ export function substituteUserConfig( } /** - * Substitute ${user_config.*} placeholders in env vars + * Substitute ${user_config.*} and ${mpak.*} placeholders in env vars */ export function substituteEnvVars( env: Record | undefined, userConfigValues: Record, + mpakVars?: Record, ): Record { if (!env) return {}; const result: Record = {}; for (const [key, value] of Object.entries(env)) { - result[key] = substituteUserConfig(value, userConfigValues); + let substituted = substituteUserConfig(value, userConfigValues); + if (mpakVars) { + substituted = substituteMpakVars(substituted, mpakVars); + } + result[key] = substituted; } return result; } @@ -453,11 +475,22 @@ export async function handleRun( ); } - // Substitute user_config placeholders in env vars + // Resolve workspace early so bundles can reference it via ${mpak.workspace} + const workspace = resolveWorkspace(process.env["MPAK_WORKSPACE"], process.cwd()); + + // Runtime variables available to mcp_config.env via ${mpak.*} + const mpakVars: Record = { + workspace, + cache_dir: cacheDir, + bundle_name: packageName, + }; + + // Substitute user_config and mpak placeholders in env vars // Priority: process.env (from parent like Claude Desktop) > substituted values (from mpak config) const substitutedEnv = substituteEnvVars( mcp_config.env, userConfigValues, + mpakVars, ); let command: string; @@ -520,18 +553,8 @@ export async function handleRun( throw new Error(`Unsupported server type: ${type as string}`); } - // Provide a project-local workspace directory for stateful bundles. - // Defaults to $CWD/.mpak — user can override via MPAK_WORKSPACE in their environment. - env["MPAK_WORKSPACE"] = resolveWorkspace(env["MPAK_WORKSPACE"], process.cwd()); - - // If the bundle is an Upjack app, set UPJACK_ROOT so entity data persists - // outside the bundle cache. - const manifestAny = manifest as unknown as Record; - const upjackMeta = manifestAny["_meta"] as Record | undefined; - const upjackExt = upjackMeta?.["ai.nimblebrain/upjack"] as Record | undefined; - if (upjackExt?.["namespace"]) { - env["UPJACK_ROOT"] = env["MPAK_WORKSPACE"]!; - } + // Ensure MPAK_WORKSPACE is always available in the child environment + env["MPAK_WORKSPACE"] = workspace; // Spawn with stdio passthrough for MCP const child = spawn(command, args, { From 4c3360288523ae9bc8b30cfa549b787a5ac94e3a Mon Sep 17 00:00:00 2001 From: Mathew Goldsborough <1759329+mgoldsborough@users.noreply.github.com> Date: Sat, 28 Mar 2026 15:33:04 -1000 Subject: [PATCH 3/3] Document ${mpak.*} runtime variable placeholders in manifest and CLI docs --- .../src/content/docs/bundles/manifest.mdx | 25 +++++++++++++++---- apps/docs/src/content/docs/cli/run.mdx | 12 +++++++++ 2 files changed, 32 insertions(+), 5 deletions(-) diff --git a/apps/docs/src/content/docs/bundles/manifest.mdx b/apps/docs/src/content/docs/bundles/manifest.mdx index e94815b..2663be3 100644 --- a/apps/docs/src/content/docs/bundles/manifest.mdx +++ b/apps/docs/src/content/docs/bundles/manifest.mdx @@ -140,13 +140,28 @@ Use `${__dirname}` to reference the bundle's extraction directory: } ``` -### Runtime Environment Variables +### Runtime Variables -`mpak run` automatically sets environment variables that bundles can use at runtime: +Use `${mpak.*}` placeholders in `mcp_config.env` to reference mpak runtime values. These are resolved before the bundle is spawned: -| Variable | Description | -|----------|-------------| -| `MPAK_WORKSPACE` | Project-local directory for persistent data (defaults to `$CWD/.mpak`). Use this instead of relative paths for any data that should survive across restarts. | +| Placeholder | Description | +|-------------|-------------| +| `${mpak.workspace}` | Project-local workspace directory (defaults to `$CWD/.mpak`) | +| `${mpak.cache_dir}` | Bundle's cache/extraction directory | +| `${mpak.bundle_name}` | Bundle name (e.g., `@scope/name`) | + +```json +{ + "mcp_config": { + "env": { + "DATA_ROOT": "${mpak.workspace}", + "CACHE_PATH": "${mpak.cache_dir}/state" + } + } +} +``` + +`mpak run` also sets `MPAK_WORKSPACE` in the child environment automatically. Bundles can read it directly at runtime if they don't use `mcp_config.env`: ```python import os diff --git a/apps/docs/src/content/docs/cli/run.mdx b/apps/docs/src/content/docs/cli/run.mdx index 9bb99a4..789021c 100644 --- a/apps/docs/src/content/docs/cli/run.mdx +++ b/apps/docs/src/content/docs/cli/run.mdx @@ -81,6 +81,18 @@ workspace = os.environ.get("MPAK_WORKSPACE", ".") const workspace = process.env.MPAK_WORKSPACE ?? "."; ``` +### Manifest Placeholders + +Bundles can also reference runtime values in `mcp_config.env` using `${mpak.*}` placeholders. These are resolved before the bundle is spawned: + +| Placeholder | Description | +|-------------|-------------| +| `${mpak.workspace}` | Same as `MPAK_WORKSPACE` | +| `${mpak.cache_dir}` | Bundle's cache/extraction directory | +| `${mpak.bundle_name}` | Bundle name (e.g., `@scope/name`) | + +See the [Manifest Reference](/bundles/manifest#runtime-variables) for usage examples. + ## Configuration Some bundles require configuration (API keys, etc.). See [mpak config](/cli/config) for storing configuration values.