@agimon-ai/po-mcp

Domain-aware MCP server selection and access boundaries for Pi sessions composed with Po.

Packages

Package details

extension

Install @agimon-ai/po-mcp from npm and Pi will load the resources declared by the package manifest.

$ pi install npm:@agimon-ai/po-mcp
Package
@agimon-ai/po-mcp
Version
0.0.0-alpha.40
Published
Oct 21, 2026
Downloads
3,601/mo · 3,601/wk
Author
agiflow-ai
License
BUSL-1.1
Types
extension
Size
1.6 MB
Dependencies
15 dependencies · 1 peer

Security note

Pi packages can execute code and influence agent behavior. Review the source before installing third-party packages.

README

@agimon-ai/po-mcp

Domain-aware MCP selection for Po.

Part of the Po distribution.

The package removes disallowed servers before any stdio process is spawned. A filtered server is absent rather than merely hidden from the model.

Alpha: configuration and adapter contracts may change between releases.

Requirements

  • Node.js 24 or newer

Install

po init include this adapter in default.packages in .po/modes.yaml. Remove it or move it to a named layer to change which major modes load it. When loaded in Po, domain selection controls server access.

Entry Purpose
@agimon-ai/po-mcp Library API
@agimon-ai/po-mcp/projection Neutral projection adapter and Agent Plugin normalization

Configure direct servers

Repository .mcp.json entries describe direct MCP servers:

{
  "mcpServers": {
    "filesystem": {
      "command": "npx",
      "args": ["-y", "@modelcontextprotocol/server-filesystem", "."]
    }
  }
}

Your own servers live in mcp.json in your Po home (~/.po/mcp.json, or under PO_HOME), in the same shape. Po loads them into every repository's sessions, first, so a repository, plugin or domain entry with the same name wins.

Add a server from the cockpit without editing either file: Add server on a repository's MCP servers page writes that repository's .mcp.json, and Settings, MCP servers writes your own config. Remote servers take a URL and optional headers. A local command also needs the trust confirmation, which the hub checks again. Adding never replaces an existing name or rewrites a file that is not valid JSON. Selected legacy plugins may also provide root .mcp.json. A schema-gated Agent Plugin v1 instead provides root mcp.json. Po validates its portable contract, supplies private persistent PLUGIN_DATA, and resolves plugin-relative stdio commands and working directories. It also normalizes streamable-http for the embedded runtime. Invalid portable server entries are isolated from the other entries in that plugin.

Proxy upstreams are read from mcp-config.yaml. Domains can select direct servers and proxy upstreams separately in .po/domains.yaml:

# .po/domains.yaml
domains:
  development:
    description: Repository development tools.
    mcp:
      servers: [filesystem]
      proxy: [github]

An absent or empty allowlist retains configured entries. Use po --no-mcp when no MCP server should load.

Within Po, this package consumes the immutable poMcpProjection service published on the session Cordis root. Downstream clients run in-process through @agimon-ai/po-mcp-proxy; this adapter does not start its Hono server.

Direct tools and configured identity

Main sessions and native child agents receive permitted named MCP tools with their descriptions and complete input schemas. Names use the configured entry prefix, not the downstream-reported server name: personal/search is exposed as personal_search and work/search as work_search. Non-alphanumeric prefix characters become underscores, preserving existing names and collision diagnostics.

Two configured entries may point to the same URL with different accounts. Each uses its own configured-name connection and credential slot. Requests retain the exact configured name and original downstream tool name; tool prefixes are not parsed to route calls.

Children borrow the parent runtime. Requested mcp or mcp_use groups expand into permitted direct tools; mcpDirectTools selectors (*, a configured server, or server/tool) narrow that set. Capability ceilings must explicitly allow MCP and still constrain the selected tools. The direct connection-management tool is main-agent-only. Borrowed trusted exec still uses parent-session bindings, including Help-only mcpTest. Remote mcp_use remains a compatibility route, not the default native model declaration.

Select domains and child subsets to control tool availability. Keep catalogs out of prompt prose.

Po MCP namespace

Po's own discover and exec tools (@agimon-ai/po-codemode) enable the mcp namespace alongside namespaces such as browser. Pi's internal MCP codemode and tool search remain disabled. These are separate mechanisms, also distinct from code mode run by a remote MCP client. While code mode is bound, the main agent's direct tool declarations for these servers are withdrawn, since the namespace reaches every one and their schemas would only cost context; they show as inactive in Context. A tool that carries an MCP App keeps its direct tool for the inline UI. Without code mode every direct tool stays, and the mcp_use compatibility route remains. Native child sessions still receive the direct tools they are granted. The namespace does not change MCP access grants.

The always-active codemode-how-to-mcp skill teaches agents this flow and, when loaded, lists the session's servers and tool names.

Start with discover({ namespace: 'mcp' }) to inspect the permitted catalog and typed signatures. If the declaration output exceeds 24,000 characters, narrow discovery with discover({ query: 'github/search_issues' }) before calling a tool. Use only names and argument schemas returned by discovery, never guessed names.

Inside exec, call a typed wrapper such as await mcp.github.search_issues(args), where args matches the discovered input schema. Names are exact configured server and original tool names, not normalized direct-tool prefixes. Use bracket access for names that need it, for example await mcp['work-github']['search-issues'](args). The generic route is await mcp.call(server, tool, args), using the exact discovered server and tool names. Both routes use the same permitted session connections and admission checks as direct calls.

Results expose text, images, structuredContent, and blocks. Inspect the text or structured content in the script; use image(result) to emit its images from exec. Upstream isError results reject with the upstream text rather than returning a successful result.

Outbound file parameters still require consent. Time waiting for that consent counts against the exec timeout: 30 seconds by default, with timeoutMs up to 120 seconds. After a runtime swap, an old run cannot access the replacement runtime. Discover again and retry in a new exec, not in the old run.

MCP Apps stay on direct tools so their inline UI and interaction lifecycle remain available. Use a direct call when an App is needed, not the namespace route.

Session-only URL test servers

In Help mode, ask permission, then discover mcpTest and add a URL server:

await mcpTest.add({ name: 'test', url: 'https://example.com/mcp', type: 'http' });

HTTP and SSE servers accept optional headers and OAuth client settings. Sign-in links appear in the existing MCP panel. Add waits for completion and respects cancellation. With no code mode, the main-agent-only mcp_test_server tool supports add, remove and list instead. These management surfaces disappear when Help turns off, but connected test servers remain. mcpTest.remove(name) disconnects and forgets one while Help is on.

Test server configuration, headers and credentials stay in memory, including across MCP reloads. They are not written to config, keyring, credential files or the shared tool cache, and a new session does not inherit them. They cannot replace configured names or override --no-mcp. Stdio and /mcp add are deliberately unsupported.

Memory-only storage does not redact tool arguments or conversation history. Never paste literal secrets into management calls. Use ${ENV_VAR} references from the session's admitted environment for sensitive headers or OAuth client settings.

Configured-server credentials continue using keyring with encrypted-file fallback. A recovered keyring reconciles newer fallback credentials on reads; the next successful write retires the fallback copy. Near-expiry tokens refresh before requests, with no idle timer. Healthy durable stores coordinate refresh across processes; degraded or explicit memory stores synchronize only within their process.

Inline Apps (development)

The conversation renderer recognizes standard _meta.ui.resourceUri, deprecated ui/resourceUri, and ChatGPT openai/outputTemplate declarations. HTML stays inside an opaque, sandboxed inline frame; ordinary text results remain available. Browsers without iframe CSP enforcement use the text fallback.

Apps open read-only, including replayed history. Enable interactions to authorize a fresh connection-bound lease. Each tool call and attributed follow-up requires host confirmation and the existing session admission checks. Calls are limited to current, app-visible tools on the original configured server. Visibility is not consent. Private result metadata and bounded widget state are not injected into model content. Reopening a widget reads its retained result and state, never reruns the original tool.

window.openai is present in every widget, beside the MCP Apps bridge, so standard Apps built on the ChatGPT SDK work too. Only ChatGPT widgets receive toolOutput globals updates after their own callTool. Each App root gets a light/dark class, data-theme and color-scheme (an App's own class or data-theme is kept), and theme changes sync live. An MCP tool card starts collapsed; an App stays visible. Expanding the card shows the call's input, then its result.

The ChatGPT adapter implements tool input/output and private metadata globals, globals updates, callTool, setWidgetState, sendFollowUpMessage, inline display mode, height, close, and confirmed external links. File libraries, checkout, accounts, modal/fullscreen modes, stable custom origins, sampling, and automatic model-context updates are unsupported.

The runtime uses workspace-owned @agimon-ai/po-mcp-proxy from packages/utils/po-mcp-proxy for Apps capability negotiation, private result capture before output guarding, and upstream request cancellation. Model-facing output remains guarded; retained App results are bounded separately. No spill-file reads are used.

Run from the repository root:

pnpm --filter @agimon-ai/po-mcp test
pnpm --filter @agimon-ai/po exec playwright test --config playwright.app.config.ts inlineMcpSandbox.spec.ts
pnpm nx run @agimon-ai/po:test:app

The stdio fixture verifies discovery, visibility, resource reads, pre-guard private results, and upstream cancellation. Browser tests cover sandbox/bootstrap isolation and standalone exported widgets. The production conversation fixture uses an isolated home and a local scripted model to verify standard and ChatGPT Apps, confirmed callbacks and attributed follow-ups, private metadata exclusion, saved state, read-only replay without rerunning tools, and a real Po-exported session widget.

Commands

/mcp
/mcp status
/mcp auth <server>
/mcp reload

The web Context panel lists configured servers and their named tools, including authorization and management actions. Open a tool to inspect its description, full input schema, warnings and estimated schema tokens. Authorization sends one /mcp auth <server> prompt frame; headless hosts never open a desktop browser automatically.

The compact po-mcp-session-auth status carries configured names, states, safe authorization URLs and tool identities/estimates. It excludes credentials and credential-store data. Repository catalog and authorization APIs remain separate from this live-session view.

Credentials and trust

Allowed stdio entries execute their configured commands with the Pi process environment and operating system privileges. Review configuration as executable code.

OAuth credentials use the operating system keyring when available. The private-file fallback is stored under ~/.mcp-proxy/oauth with owner-only permissions. Credentials remain machine-wide and keyed by server name, while their upstream URL binding prevents blind replay to another endpoint. Treat both the configuration and credential store as sensitive.

The default OAuth callback is http://127.0.0.1:19876/callback. Authorization therefore requires the browser and Po runtime to share that loopback network namespace. A remote browser cannot complete the callback when its 127.0.0.1 resolves to another machine or container.

Public API

import { buildMcpConfigGroups, readCachedCatalog, toPiToolName } from '@agimon-ai/po-mcp';
import type { McpAllowlist, McpSessionConfig } from '@agimon-ai/po-mcp';

po --emit-mcp <directory> emits the resolved MCP configuration to the target directory without launching a model.

Development

pnpm build
pnpm typecheck
pnpm test
pnpm lint

Maintained by Agimon.

License

Business Source License 1.1: free for personal use, organisations under USD 1M revenue, non-profits and education. See LICENSE and https://agimon.ai/licensing.

Named routed src/extensions/ entries declare host contributions and delegate to services. MCP session state and connection behavior live in named service folders, and src/exports/ exposes reusable public APIs.

Remote file transfer

Session MCP always exposes share_file and save_file, both marked with _meta['po/fileTransfer']=true, subject to the connection's capability grants. share_file({path}) uses session-bound host sharing without a consent prompt. Its structured result contains url, fileName, mimeType, size, expiresAt (epoch milliseconds), and maxDownloads (3). Links expire after 10 minutes and require remote access. Outbound file parameters sent to other MCP servers still require consent.

save_file declares openai/fileParams=['file'] and accepts exactly one file form: {download_url,file_name,file_id?,mime_type?} or {name,data_base64}. HTTPS downloads accept only public addresses, pin DNS for each request, revalidate up to three redirects, and have a 30-second deadline and 25 MiB cap. Base64 is validated before decoding and capped at 5 MiB. Without path, files become private session attachments using the bound session environment. Optional path is relative to the bound cwd, requires existing non-symlink parents, and creates a private file exclusively, never overwriting. Results contain path, name, size, and mimeType; supplied MIME hints are not trusted as content detection.

Workspace containment uses portable identity and realpath rechecks, not atomic descriptor-relative operations. This mode assumes no hostile concurrent workspace mutation: ancestor swaps can create a file outside cwd or expose bytes before a later check rejects the write. Failed cwd writes truncate the exclusively created descriptor but may leave an empty private file. Cleanup never unlinks a mutable pathname, which could delete an unrelated replacement. Remove any empty artifact before retrying the same destination. Session attachments retain their normal temporary-file cleanup.