Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 20 additions & 5 deletions apps/docs/src/content/docs/bundles/manifest.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
12 changes: 12 additions & 0 deletions apps/docs/src/content/docs/cli/run.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
76 changes: 76 additions & 0 deletions packages/cli/src/commands/packages/run.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
resolveArgs,
resolveWorkspace,
substituteUserConfig,
substituteMpakVars,
substituteEnvVars,
getLocalCacheDir,
localBundleNeedsExtract,
Expand Down Expand Up @@ -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", () => {
Expand Down Expand Up @@ -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");
});
});
44 changes: 38 additions & 6 deletions packages/cli/src/commands/packages/run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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, string>,
): 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'
Expand All @@ -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<string, string> | undefined,
userConfigValues: Record<string, string>,
mpakVars?: Record<string, string>,
): Record<string, string> {
if (!env) return {};
const result: Record<string, string> = {};
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;
}
Expand Down Expand Up @@ -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<string, string> = {
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;
Expand Down Expand Up @@ -520,9 +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());
// 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, {
Expand Down
Loading