Skip to content
Merged
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
1 change: 1 addition & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ Tests validate structural invariants like version consistency between `package.j
- ESM throughout (`"type": "module"` in package.json, `.js` extensions in imports)
- TypeScript strict mode, target ES2022, Node16 module resolution
- The build produces both `dist/` (tsc output) and `plugin/scripts/bundle.js` (esbuild single-file bundle); the bundle is what end users run
- `scripts/build-bundle.mjs` fails when the bundle reaches 1 MiB, the size the Claude plugin directory reads as text; a larger bundle holds every version for a reviewer. Keep the bundle readable: trim dependencies instead of minifying. It already keeps only zod's English locale and leaves out the MCP SDK's ajv validator, which the SDK's server uses only for form elicitation, so the server must not use elicitation (`plugin.test.ts` checks this).
- Authentication uses the device authorization code flow. `login_start` → user approves in browser → `login_wait` stores the token at `~/.config/deploygate/token` (0600). `logout` revokes server-side and deletes the file.
- Skill `allowed-tools` must name each MCP tool with the plugin scope (`mcp__plugin_deploygate_deploygate__<tool>`), never a wildcard. Every entry must be on the `PRE_APPROVABLE_RULES` allowlist in `skills.test.ts`, which leaves out destructive or access-granting MCP tools, build tools (`./gradlew`, `xcodebuild`, `fastlane`), shell commands that write files or run arbitrary code, and unscoped `Write`/`Edit` (use `Edit(<path>)`, which also covers `Write`). The Claude plugin directory holds versions that pre-approve broad access, so extend the allowlist only deliberately.
- The `members.ts` `add_member` tool orchestrates multiple API calls (workspace → project → team) in a single tool invocation, handling "already exists" gracefully
4 changes: 4 additions & 0 deletions plugin/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@
},
"homepage": "https://deploygate.com",
"license": "MIT",
"documentationUrl": "https://github.com/DeployGate/deploygate-agent-plugin#readme",
"supportUrl": "https://github.com/DeployGate/deploygate-agent-plugin/blob/main/SUPPORT.md",
"privacyPolicyUrl": "https://deploygate.com/terms/privacy",
"termsOfServiceUrl": "https://deploygate.com/terms",
"keywords": [
"deploygate",
"mobile",
Expand Down
9 changes: 6 additions & 3 deletions plugin/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,12 +44,15 @@ The plugin runs a local MCP server, so its tools work in Claude Code and Cowork

## About `scripts/bundle.js`

`scripts/bundle.js` is the MCP server and its npm dependencies, mainly `@modelcontextprotocol/sdk` and `zod`, bundled into one unminified file with esbuild. It is built from the TypeScript source in [`src/`](https://github.com/DeployGate/deploygate-agent-plugin/tree/main/src) by `npm ci && npm run bundle`, and CI checks that the committed bundle matches a fresh build of the source before each release.
`scripts/bundle.js` is the MCP server and its npm dependencies, mainly `@modelcontextprotocol/sdk` and `zod`, bundled into one unminified file with esbuild. The build leaves out code the server doesn't use: zod's error messages in languages other than English, and the SDK's JSON Schema validator for form elicitation. It is built from the TypeScript source in [`src/`](https://github.com/DeployGate/deploygate-agent-plugin/tree/main/src) by `npm ci && npm run bundle`, and CI checks that the committed bundle matches a fresh build of the source before each release, and that it stays under 1 MiB so it can be read as text.

## Support

- Source and issues: https://github.com/DeployGate/deploygate-agent-plugin
- Help: help@deploygate.com
- Questions about the DeployGate service: [DeployGate Help Center](https://intercom.help/deploygate)
- Bugs and feature requests for this plugin: [GitHub Issues](https://github.com/DeployGate/deploygate-agent-plugin/issues)
- Security vulnerabilities: [help@deploygate.com](mailto:help@deploygate.com) (please don't open a public issue)

See [SUPPORT.md](https://github.com/DeployGate/deploygate-agent-plugin/blob/main/SUPPORT.md) for details.

## License

Expand Down
55 changes: 52 additions & 3 deletions scripts/build-bundle.mjs
Original file line number Diff line number Diff line change
@@ -1,19 +1,68 @@
import { build } from "esbuild";
import { readFile } from "node:fs/promises";
import { readFile, stat } from "node:fs/promises";
import { fileURLToPath } from "node:url";
import { dirname, resolve } from "node:path";
import { dirname, resolve, sep } from "node:path";

const here = dirname(fileURLToPath(import.meta.url));
const root = resolve(here, "..");
const pkg = JSON.parse(await readFile(resolve(root, "package.json"), "utf-8"));
const outfile = resolve(root, "plugin/scripts/bundle.js");

// The Claude plugin directory reads files only up to 1 MiB as text; a larger
// bundle can't be inspected and holds every version for a reviewer.
const MAX_BUNDLE_BYTES = 1024 * 1024;

// zod re-exports every error-message locale as `z.locales`, which adds about
// 280 KB that the server never uses: zod configures English itself, and
// nothing switches locales. Keep only English.
const zodEnglishLocaleOnly = {
name: "zod-english-locale-only",
setup(build) {
const zodLocalesDir = `${sep}node_modules${sep}zod${sep}v4${sep}locales${sep}`;
build.onLoad({ filter: /[\\/]zod[\\/]v4[\\/]locales[\\/]index\.js$/ }, (args) =>
args.path.includes(zodLocalesDir)
? { contents: 'export { default as en } from "./en.js";', loader: "js" }
: undefined,
);
},
};

// On the server side, the MCP SDK uses ajv (about 270 KB with its
// dependencies) only to validate form elicitation responses, and this server
// never elicits. Swap in a validator that fails loudly if that ever changes.
const sdkWithoutAjv = {
name: "sdk-without-ajv",
setup(build) {
build.onLoad(
{ filter: /[\\/]@modelcontextprotocol[\\/]sdk[\\/]dist[\\/]esm[\\/]validation[\\/]ajv-provider\.js$/ },
() => ({
contents: `export class AjvJsonSchemaValidator {
getValidator() {
throw new Error("JSON Schema validation (ajv) is left out of the DeployGate MCP bundle; see scripts/build-bundle.mjs");
}
}`,
loader: "js",
}),
);
},
};

await build({
entryPoints: [resolve(root, "dist/index.js")],
bundle: true,
platform: "node",
format: "esm",
outfile: resolve(root, "plugin/scripts/bundle.js"),
outfile,
plugins: [zodEnglishLocaleOnly, sdkWithoutAjv],
define: {
__PLUGIN_VERSION__: JSON.stringify(pkg.version),
},
});

const { size } = await stat(outfile);
if (size >= MAX_BUNDLE_BYTES) {
throw new Error(
`plugin/scripts/bundle.js is ${size} bytes, at or over the ${MAX_BUNDLE_BYTES}-byte limit ` +
"the Claude plugin directory reads as text. Trim dependencies instead of minifying.",
);
}
33 changes: 32 additions & 1 deletion src/__tests__/plugin.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, it, expect } from "vitest";
import { readFileSync, existsSync } from "node:fs";
import { readFileSync, existsSync, readdirSync } from "node:fs";
import { resolve } from "node:path";

const ROOT = resolve(import.meta.dirname, "../..");
Expand Down Expand Up @@ -78,6 +78,20 @@ describe("plugin/.claude-plugin/plugin.json", () => {
expect(plugin.skills).toBe("./skills/");
expect(plugin.mcpServers).toBe("./.mcp.json");
});

// Anthropic's directory reads its listing links from these fields, and
// drops any link that isn't https://. Without them it guesses from the
// README.
it("sets https:// directory listing links", () => {
for (const field of [
"documentationUrl",
"supportUrl",
"privacyPolicyUrl",
"termsOfServiceUrl",
]) {
expect(plugin[field]).toMatch(/^https:\/\/\S+$/);
}
});
});

describe(".claude-plugin/marketplace.json", () => {
Expand Down Expand Up @@ -303,3 +317,20 @@ describe("version sync across release manifests", () => {
expect(manifest["."]).toBe(version);
});
});

// scripts/build-bundle.mjs leaves the MCP SDK's ajv validator out of the
// bundle, because the SDK's server uses it only to validate form elicitation
// responses. Elicitation would fail at runtime in the bundle, so the server
// must not use it.
describe("bundle build assumptions", () => {
it("server source does not use elicitation", () => {
const dirs = ["src", "src/tools"];
for (const dir of dirs) {
for (const file of readdirSync(resolve(ROOT, dir))) {
if (!file.endsWith(".ts")) continue;
const source = readFileSync(resolve(ROOT, dir, file), "utf-8");
expect(source, `${dir}/${file}`).not.toMatch(/elicit/i);
}
}
});
});
Loading