Skip to content

feat(github-mcp): add pi as a third host - #12

Merged
Martin Bens (SpiGAndromeda) merged 11 commits into
mainfrom
feat/pi-support
Oct 2, 2026
Merged

Martin Bens (SpiGAndromeda) merged 11 commits into
mainfrom
feat/pi-support

Conversation

@SpiGAndromeda

@SpiGAndromeda Martin Bens (SpiGAndromeda) commented Oct 2, 2026 •

Copy link
Copy Markdown
Collaborator

github-mcp runs in pi 1.0.0 next to Claude Code and Codex. The repository root becomes the npm package @shopware-ag/github-agent-tools, installable with one of:

pi install npm:@shopware-ag/github-agent-tools
pi install git:github.com/shopwareLabs/github-agent-tools

The pi extension

plugins/github-mcp/pi/index.ts adapts the existing runtime to pi's extension API; the shell scripts stay the single implementation of the gates and the directive.

  • Servers. Registers gh-tooling and gh-tooling-write with exposure: "deferred". pi lists both servers in its mcp_servers system-prompt section and loads tools through tool_search, so 56 tool schemas do not sit in every request. The write server is always registered and serves an empty list unless enable_write_server is set, the same as on the other hosts. A gh-tooling entry in pi's mcp.json overrides the registration; the README states that such an entry must repeat command and set GITHUB_MCP_HOST=pi.
  • Directive. Runs session-start.sh on every session_start and writes the result to a github_mcp system-prompt section. On pi the directive gains hooks/prompts/host-pi.md, which names the mcp__gh_tooling__… tool form.
  • Gates. On tool_call, bash calls go through check-gh-tools.sh, and mcp__gh_tooling__api_read / mcp__gh_tooling_write__api through check-api-tools.sh. This covers calls made from codemode scripts too. pi/gate.ts runs each script in its own process group with GITHUB_MCP_HOST=pi and without CLAUDE_PROJECT_DIR, kills the group on timeout, and blocks only on exit 2. Any other outcome lets the call through, matching how Claude Code and Codex treat a hook that exits with anything but 2.
  • The extension imports pi packages with import type only and has no runtime dependencies; pi loads the .ts file directly.

Config locations and host resolution

  • .pi/.mcp-gh-tooling.json is a new config location. One rule in plugins/github-mcp/shared/config-dirs.sh gives the order for every host: the active host's directory, then the rest of .claude/, .codex/, .pi/, then the project root. The hooks and both servers use it; _load_gh_config now lives once in mcp-server-gh/lib/common.sh. For Claude Code and Codex users this means a .pi/ file now ranks ahead of the project-root file, the same way .codex/ already ranked ahead of it for Claude Code.
  • CLAUDE_PROJECT_DIR still selects Claude Code first. GITHUB_MCP_HOST=pi|codex only applies when it is unset, so a host variable exported in a shell cannot misroute Claude Code's hooks. pi needs nothing more, because the extension drops CLAUDE_PROJECT_DIR before running any script. The related Codex case is github-mcp hooks resolve as Claude Code under Codex when CLAUDE_PROJECT_DIR is inherited #11.

Release

.github/workflows/npm-publish.yml publishes on a pushed v* tag via npm trusted publishing (OIDC, no stored token). It first checks the tag against package.json and both plugin manifests. Trusted publishing can only be configured for a package that already exists, so the first version needs a one-time manual npm publish by a maintainer of the @shopware-ag npm org, followed by npm trust github … (the command is in AGENTS.md §Distribution). renovate.json groups @earendil-works/pi-coding-agent and @earendil-works/pi-ai on one exact version and holds typescript below 6.1.0; both rules take effect once Renovate is enabled for the repository.

CI adds Node 24, npm ci, a type-check, ESLint, and the Node unit tests. ESLint runs typescript-eslint's strict type-checked presets plus unicorn, sonarjs, perfectionist, promise, regexp, and n, with --max-warnings 0. typescript is pinned to 6.0.3 rather than 7 because typescript-eslint supports only versions below 6.1.0. The Node unit tests cover gate.ts and the extension's event handlers on a fake pi, including malformed session-start output and calls the gates let through. pi_e2e.bats drives the real pi binary against a scripted model and a stubbed gh, in both the git-clone and npm-tarball install layouts. The branch also bumps actions/checkout to v7.0.1 and bats-core to 1.14.0.

github-mcp now runs in pi 1.0.0 as the npm package @shopware-ag/github-agent-tools, rooted at the repository. The extension in plugins/github-mcp/pi/ registers the existing gh-tooling and gh-tooling-write servers with deferred exposure, injects the SessionStart directive as the github_mcp system-prompt section, and runs check-gh-tools.sh and check-api-tools.sh on pi's tool_call event. gate.ts spawns each script in its own process group with GITHUB_MCP_HOST=pi and without CLAUDE_PROJECT_DIR, kills the group on timeout, and fails open on anything but exit 2, so the shell scripts stay the single implementation of the gates and the directive.

The hooks and servers gain a .pi/.mcp-gh-tooling.json location. One rule in shared/config-dirs.sh now produces the config order for every host: the active host's directory, then the rest of .claude/, .codex/, .pi/, then the project root. _load_gh_config moves into mcp-server-gh/lib/common.sh instead of being duplicated in both servers. CLAUDE_PROJECT_DIR still selects Claude Code ahead of GITHUB_MCP_HOST, so an exported host variable cannot misroute Claude Code's hooks.

Tests cover the pi host and the new config location in BATS, gate.ts with node:test unit tests, and the whole package in pi_e2e.bats, which drives the real pi binary with a scripted faux model and a stubbed gh against both the git-clone and npm-tarball install layouts. CI gains Node 24, npm ci, the type-check, and the unit tests. npm-publish.yml publishes on v* tags through npm trusted publishing after checking the tag against all three version files. The version is 4.4.0 in package.json and both plugin manifests.

Co-Authored-By: Claude <noreply@anthropic.com>
The Testing table listed 8 of the 29 suites under plugin-tests/github-mcp/, and none of the pi suites added with pi support (pi_e2e.bats, package_contents.bats, package_manifest.bats, pi/gate.test.ts). It now has one row per suite, each written from the suite's own test names. The run command matched only top-level .bats files and skipped the Node tests. It is replaced by a pointer to the root AGENTS.md sections that own the commands and the pi_e2e.bats prerequisites.

The directory tree also gains files it was missing: SETUP.md, the four hooks/prompts/*.md files besides mcp-tool-directives.md, and mcp-server-gh/tools-empty.json, which the write server reports as its tool list while enable_write_server is false.

Co-Authored-By: Claude <noreply@anthropic.com>
write_server_gating.bats and tool_dispatch_declaration.bats started server-read.sh and server-write.sh in place under mcp-server-gh/. Both servers set MCP_LOG_FILE to a file next to themselves, so every BATS run appended to mcp-server-gh/server.log and server-write.log, and the tools-empty check in tool_dispatch_declaration.bats looked at the real directory. Both suites now copy the plugin into BATS_TEST_TMPDIR in setup() and run the copy, the pattern server_startup_tools_list.bats already uses. The tools-empty check inspects the copy the server ran in. Read-only greps of the real lib files and tool lists are unchanged.

Co-Authored-By: Claude <noreply@anthropic.com>
Adds eslint.config.mjs with typescript-eslint's strictTypeChecked and stylisticTypeChecked presets plus unicorn, sonarjs, perfectionist, promise, regexp, and n, and runs `npx eslint . --max-warnings 0` in ci.yml and npm-publish.yml after the type-check.

typescript moves from 7.0.2 to 6.0.3 because typescript-eslint 8.71.0 accepts only typescript below 6.1.0 and its typed rules need the compiler API that TypeScript 7 does not ship. A Renovate rule holds typescript below 6.1.0 until that changes.

tsconfig enables noUncheckedIndexedAccess, which makes the driver's unknown-scenario guard reachable. The four TypeScript files are adjusted to the rules: sorted imports and keys, braces on every branch, boolean names with an is- prefix, and a default import for node:path. Spawning bash from PATH keeps an inline disable for sonarjs/no-os-command-from-path, because the hooks get the same bash on Claude Code and Codex.

Co-Authored-By: Claude <noreply@anthropic.com>
actions/checkout v7 only blocks fork checkouts on pull_request_target and workflow_run, which no workflow here uses. bats-core 1.14 makes `run` honor `set -e` inside functions; the full suite passes under it.

Co-Authored-By: Claude <noreply@anthropic.com>
pi_e2e.bats covers the happy paths: the directive reaching the system prompt and the gates blocking gh and api_read. It does not cover the extension's own failure branches. index.test.ts registers the extension on a fake pi that records its handlers and checks that malformed session-start output or a non-zero exit adds no github_mcp section, and that bash and api_read calls the gates allow go through unblocked.

The session-start cases import a per-test copy of pi/, hooks/, and shared/ with session-start.sh replaced, so the real scripts stay untouched and the tests stay safe to run in parallel.

Co-Authored-By: Claude <noreply@anthropic.com>
…nput

runScript treated a stdin write error as a failed run and resolved `{ code: null }`. A script that exits without reading its input makes that write fail with EPIPE, so whether its exit code counted depended on which event arrived first. On Linux CI the error won, and a gate exiting 2 without reading its input would have let the call through. The stdin error is now ignored and `close` reports the exit code; a failed spawn and the timeout still resolve `{ code: null }`.

The 1 MB-input test now exits 2 and expects a block, because exiting 0 passed whichever event won.

Co-Authored-By: Claude <noreply@anthropic.com>
package_manifest.bats read package.json and the extension source back and asserted the values written there, so it failed only when someone changed them on purpose. package_contents.bats globbed the same directories that package.json `files` lists, so it could not catch a runtime file left outside them; its log check passed whether or not logs would be packed, because no log exists when CI packs. pi_e2e.bats installs the packed tarball and runs pi against it, which is where a missing file shows.

The type-only import rule for @earendil-works packages moves to ESLint as @typescript-eslint/no-restricted-imports on the pi extension. The version match between package.json and the plugin manifests stays enforced by npm-publish.yml at release time.

Co-Authored-By: Claude <noreply@anthropic.com>
…rces

Drops 12 tests that no realistic change could fail on its own. The index.test.ts block cases repeat pi_e2e.bats through the same handler and gate scripts. The npm-layout e2e cases for deferral, tool_search, parallel calls, and exit status repeat the git layout. The check_api_tools.bats pi cases pass for any host, because .pi is searched for every host. Four config-precedence cases repeat a pair that another test already pins.

pi_e2e.bats no longer compares the mcp_servers section against a copy of the server description in index.ts, and session_start.bats reads host-pi.md instead of carrying a copy of it. The two cases checking that one block_api_tool_* flag leaves the other server's tool allowed move into the generic read and write sections.

Co-Authored-By: Claude <noreply@anthropic.com>
session-start.sh appends hooks/prompts/host-<host>.md when the active host has one, instead of checking for pi by name. block_tool gives every host except Claude Code the sanitized tool names, because Claude Code is the only host that qualifies them with the plugin name. The script headers no longer list hosts.

The docs state each rule once and no longer define pi by comparison with Codex. The root AGENTS.md heading becomes "Hosts". The config order keeps its rule and drops the per-host lists. The fail-open behavior is described for every host, since Claude Code and Codex hooks also block only on exit 2. The manual-setup notes name pi alongside Codex. "Adding another plugin" notes that root package.json is the single pi package. The issue templates gain pi in the host dropdowns, steps, version commands, and config locations.

The precedence tests share one write_project_config helper in common_setup.bash and one server runner, and sit under host-neutral headings instead of separate pi blocks.

Co-Authored-By: Claude <noreply@anthropic.com>
The plugin manifests return to 4.3.0, and package.json starts at that version. The changelog entry moves back under [Unreleased] until the release commit names the version.

Co-Authored-By: Claude <noreply@anthropic.com>
@SpiGAndromeda
Martin Bens (SpiGAndromeda) merged commit c30bc08 into main Oct 2, 2026
2 checks passed
@SpiGAndromeda
Martin Bens (SpiGAndromeda) deleted the feat/pi-support branch October 2, 2026 14:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant