mcporter Documentation

Call MCPs via TypeScript, masquerading as simple TypeScript API. Or package them as cli.

4,349 stars TypeScript TypeScript 1 file ยท ~23,818 tokens #cli#mcp#ts-api
RAW Doc

1. Project Overview & Quickstart (openclaw/mcporter)

File: README.md

MCPorter ๐Ÿงณ โ€” Carry MCP tools into scripts, CLIs, and agents





MCPorter is a TypeScript runtime and command-line tool for discovering and calling Model Context Protocol servers. It is for developers and coding agents that need the same MCP tools from a terminal, a script, or a generated standalone CLI.

Install

Try the CLI without installing it:

sh
npx mcporter --version

Install it for repeated command-line use:

sh
brew install steipete/tap/mcporter
# or
npm install mcporter

Node 24 or newer is required for npm installs. For the TypeScript runtime, add mcporter to your project with your package manager. Signed macOS binaries and the other supported install paths are covered in the install guide.

Quick start

Inspect a public MCP server, then call one of its tools:

sh
npx mcporter list https://mcp.context7.com/mcp --brief
npx mcporter call https://mcp.context7.com/mcp.resolve-library-id \
  query="React hooks docs" libraryName=react

The first command prints the server's TypeScript-style tool signatures. The second returns matching Context7 library IDs without requiring local configuration or credentials. See the five-minute quickstart for resources, generated CLIs, and typed clients.

Core workflows

Goal Command or API Details
Discover servers and tools mcporter list CLI reference
Call tools and read resources mcporter call, mcporter resource Call syntax
Connect to a one-off URL or stdio command --http-url, --stdio Ad-hoc servers
Complete or seed OAuth mcporter auth, mcporter vault Configuration
Generate a focused CLI mcporter generate-cli CLI generator
Generate TypeScript types or clients mcporter emit-ts Typed clients
Capture a reproducible MCP session mcporter record, mcporter replay Record and replay

Human-readable output goes to stdout by default. Use JSON output when another program or agent needs a stable result, and consult mcporter <command> --help for the flags supported by that command.

Configuration and discovery

MCPorter reads project and user config, then imports MCP servers from Cursor, Claude Code and Desktop, Codex, Windsurf, OpenCode, and VS Code. A minimal config/mcporter.json looks like this:

json
{
  "mcpServers": {
    "context7": {
      "url": "https://mcp.context7.com/mcp"
    }
  }
}

Config files accept JSONC, environment placeholders, HTTP and stdio definitions, OAuth settings, tool filters, and lifecycle policy. The configuration guide defines precedence and the full schema; the import reference lists every discovered client format.

Chrome DevTools definitions using --autoConnect can control Chrome through a paired OpenClaw extension relay. MCPorter discovers OpenClaw's active extension relay endpoint, while preserving an explicit MCPORTER_CHROME_DEVTOOLS_RELAY_URL override and the legacy 127.0.0.1:18799 fallback. It uses Browser Relay Authentication v2 over one retained loopback socket, keeps the host key out of the network and child process, and gives chrome-devtools-mcp a credential-free loopback URL plus ephemeral authorization through an OS-protected preload handoff. Old relay authentication is never retried. Routing defaults to prefer, which may fall back only to Chrome's original auto-connect path; require fails closed and off keeps original auto-connect.

TypeScript runtime

Use createRuntime() when a process needs explicit server definitions, connection reuse, or several calls:

ts
import { createRuntime } from 'mcporter';

const runtime = await createRuntime({
  servers: [{ name: 'context7', command: { kind: 'http', url: new URL('https://mcp.context7.com/mcp') } }],
});
try {
  console.log((await runtime.listTools('context7')).map((tool) => tool.name));
} finally {
  await runtime.close();
}

callOnce() handles a single configured call and cleanup. createServerProxy() maps MCP tool names to callable camelCase properties and wraps results with text, Markdown, JSON, image, and raw-content helpers. See the runtime overview and tool-calling guide.

Protocols and long-lived servers

MCPorter connects to stdio, Streamable HTTP, and legacy SSE servers. It negotiates the current 2026-07-28 protocol or a legacy revision per server, while legacy connections advertise client elicitation capabilities. Interactive CLI calls can answer form and URL requests; headless and daemon-managed calls decline them with an actionable hint.

The repository's modern and legacy fixture servers cover both generations; CI exercises representative fixture paths end-to-end over stdio and Streamable HTTP. See protocols and interactive requests for negotiation controls and the keep-alive daemon for pooled servers, the MCP bridge, and Chrome DevTools integration.

Development

sh
pnpm install --frozen-lockfile
pnpm check
pnpm test
pnpm docs:site

The project uses the Node and pnpm versions declared in package.json. Live hosted-server tests are opt-in; see live tests and manual testing.

License

MIT โ€” see LICENSE.


File: docs/adhoc.md


summary: 'How to use mcporterโ€™s ad-hoc HTTP/stdio flags to try MCP servers without editing config files.'
read_when:

  • 'Exploring or debugging unknown MCP endpoints'

Ad-hoc MCP Servers

mcporter is gaining support for "just try it" workflows where you point the CLI at a raw MCP endpoint without first editing a config file. This doc tracks the behavior and heuristics we use to make that experience smooth while keeping the runtime predictable.

Entry Points

Two new flag sets let you describe a server on the command line:

  • mcporter list --http-url https://mcp.linear.app/mcp [--name linear]
  • mcporter call --stdio "bun run ./server.ts" --name local-tools

You can also pass a bare URL as the selector (mcporter list https://mcp.linear.app/mcp) or embed the URL in a call expression (mcporter call 'https://mcp.example.com/tools.generate({ topic: "release" })').

  • Add --json to mcporter list โ€ฆ when you need a machine-readable summary of status counts and per-server failures, use --output json/--output raw with mcporter call to receive structured { server, tool, issue } envelopes whenever a transport error occurs, and run mcporter auth โ€ฆ --json to capture the same envelope if OAuth or transport setup fails.

Example: HTTP ad-hoc workflow

bash
# Inspect the server directly via URL (no config entry needed)
mcporter list 'https://mcp.sentry.dev/mcp?agent=1'

# Call a tool by repeating the same URL + tool suffix
mcporter call 'https://mcp.sentry.dev/mcp?agent=1.use_sentry(request: "yo whats up")'

Notice that the second command repeats the URL. Ad-hoc definitions are ephemeral unless you pass --persist, so there is no stored server named mcp-sentry-dev-mcp yet. Reusing the printed slug only works after you persist the definition (see "Naming & Identity" below) or add the server to your config; otherwise keep passing the URL/stdio selector each time.

Transport Detection

  • HTTP(S): Providing a URL defaults to the streamable HTTP transport. https:// works out of the box; http:// requires --allow-http (or the hidden alias --insecure) to acknowledge cleartext traffic. The --sse flag is a hidden alias for --http-url to match older examples. Use repeatable --header KEY=value flags for private servers that require headers such as Authorization, X-API-Key, or tenant selectors.
  • STDIO: Supplying --stdio (with a command string) or --stdio-bin (binary + args) selects the stdio transport. Your current shell environment is inherited automatically; use --env KEY=value only when you need to inject/override specific variables (and --cwd to change directories).
  • Conflict guard: Passing both URL and stdio flags errors out so we donโ€™t guess.

Naming & Identity

  • --name wins when provided.
  • Otherwise we derive a slug:
    • HTTP: <host> plus a sanitized path fragment (e.g. mcp-linear-app-mcp).
    • STDIO: executable basename + script (node-singlestep).
  • Until you persist the definition (via --persist, mcporter config add, etc.), the slug is only used as the cache key for OAuth tokens/log prefsโ€”you still need to supply the ad-hoc descriptor (--http-url, --stdio, or bare URL) every time you invoke mcporter list|call|auth.
  • Once persisted, the slug becomes a normal server name so mcporter call <slug>.tool and mcporter auth <slug> work just like any other configured entry.
  • If you donโ€™t persist the definition, run mcporter auth https://mcp.linear.app/mcp (or supply --name linear so mcporter auth linear also works) to finish OAuth with the same settings.

This name becomes the cache key for OAuth tokens and log preferences, so repeated ad-hoc calls still benefit from credential reuse.

OAuth Auto-Detection

Many hosted MCP servers (Supabase, Vercel, etc.) advertise OAuth capabilities but expect clients to discover this dynamically. When an ad-hoc HTTP server responds with 401/403 during the initial handshake, mcporter now:

  1. Promotes the definition to OAuth and spins up the default browser flowโ€”no need to edit config or supply auth: "oauth" manually. On headless hosts, pass --no-browser (or --browser none) to print the authorization URL instead of launching the platform browser.
  2. Persists the change whenever you pass --persist, so future runs remember that the endpoint requires OAuth without repeating the detection step.

The CLI still avoids surprise prompts during mcporter list; the upgrade happens the first time you run mcporter auth or any other command that allows OAuth (i.e., not in --autoAuthorize=false mode).

Auth & Persistence

  • OAuth flows are allowed; successful tokens store under the inferred name just like regular definitions.
  • mcporter auth accepts the same --http-url/--stdio flags (and even bare URLs), so you can immediately re-run mcporter auth https://โ€ฆ after a 401 without touching a config file.
  • Use mcporter auth --no-browser for human-in-the-loop headless OAuth. Text mode writes only the authorization URL to stdout; --json --no-browser writes authorizationUrl plus redirectUrl as JSON. Keep that URL out of durable CI logs and support bundles. The mcporter auth process must keep running until the browser redirects to redirectUrl; process managers that capture stdout and then tear down the command can kill the local callback listener before tokens are saved. Use a persistent terminal, tmux, or a supervised background process such as nohup when completing OAuth outside an interactive shell.
  • When opening the URL on a different machine, remember that loopback redirect URLs point at the browser machine unless an SSH tunnel forwards the callback port back to the mcporter process. Use oauthRedirectUrl when you need a predictable callback port.
  • Nothing is written to disk unless you pass --persist /path/to/config.json. When set, we merge the generated definition into that file (creating it if necessary) so future runs can rely on the standard config pipeline. Ad-hoc HTTP headers are persisted with the entry, so placeholders such as --header 'Authorization=$env:MY_TOKEN' keep working through the normal config header resolver.

Safety Nets

  • Non-HTTPS endpoints require --allow-http.
  • For stdio commands we print a confirmation snippet the first time we see a new command unless --yes is present.
  • Missing transports or malformed combinations throw descriptive errors, pointing to docs/adhoc.md for guidance.

Follow-ups

  • Extend mcporter config add to leverage the same helper, making it the one-stop path from exploration to permanence.
  • Consider caching inference results so repeated URL calls automatically rehydrate the previous settings (env/cwd).

File: docs/agent-skills.md


summary: 'How to expose mcporter-backed MCP servers to agents through small per-server skills.'
read_when:

  • 'Writing agent skill docs or wiring mcporter into an agent runtime'
  • 'Triaging requests for a generic mcporter skill'

Agent Skill Pattern

Prefer one small skill per MCP server or workflow instead of a single generic
mcporter skill. A focused skill keeps the agent prompt small, names the useful
tools directly, and avoids loading schemas for servers that are irrelevant to the
current task.

  1. Add or import the MCP server:

    bash
    npx mcporter config add docs https://mcp.context7.com/mcp --scope home
  2. Inspect the tool surface:

    bash
    npx mcporter list docs --brief
    npx mcporter list docs --schema
  3. Write a skill that calls only the relevant tools via mcporter call:

    markdown
    ---
    name: docs-mcp
    ---
    
    # Docs MCP
    
    Use `npx mcporter call docs.resolve-library-id query=<task> libraryName=<name>`
    to resolve a package, then call `npx mcporter call docs.query-docs ...` with
    the resolved ID and docs query.
  4. For repeated or shareable workflows, generate a dedicated CLI instead of
    teaching the agent raw mcporter call syntax:

    bash
    npx mcporter generate-cli docs --bundle dist/docs-mcp.js

Why Not One Generic Skill?

A generic skill has to teach the agent how to discover, choose, and call every
configured server. That recreates the large-schema context problem MCPorter is
trying to avoid. Per-server skills stay small and let the skill author describe
the safe, useful workflows for that server.

Use allowedTools or blockedTools in mcporter.json when a server exposes
tools that should not be shown to agents.


File: docs/call-heuristic.md


summary: 'Explains mcporter callโ€™s typo heuristics, auto-correction thresholds, and suggestion behavior.'
read_when:

  • 'Tool invocations fail due to misspelled names'

Call Command Auto-Correction

mcporter call aims to help when a tool name is almost correct without hiding real mistakes.

Confident Matches โ†’ Auto-Correct

  • We normalise tool names (strip punctuation, lowercase) and compute a Levenshtein distance.
  • If the distance is โ‰ค max(2, floor(length ร— 0.3)), or the names only differ by case/punctuation, we retry automatically.
  • A dim informational line explains the correction: [mcporter] Auto-corrected tool call to linear.list_issues (input: linear.listIssues).

Low-Confidence Matches โ†’ Suggest

  • When the best candidate falls outside the threshold we keep the original failure.
  • We still print a hint so the user learns the canonical name: [mcporter] Did you mean linear.list_issue_statuses?
  • No second call is attempted in this case.

Edge Cases

  • We only inspect the tool catalog if the server explicitly replied with โ€œTool โ€ฆ not foundโ€. Other MCP errors surface untouched.
  • If listing tools itself fails (auth, offline, etc.) we skip both auto-correct and hints.
  • Behaviour is covered by tests/cli-call.test.ts.

Server Selection Heuristic

  • mcporter list <server> now applies the same edit-distance heuristic to server names. If you type vercek, the CLI auto-corrects to vercel (and logs [mcporter] Auto-corrected server name to vercel (input: vercek).).
  • When the typo is too large, we keep the original failure but emit a hint: [mcporter] Did you mean linear? followed by the usual โ€œUnknown MCP server โ€ฆโ€ line. This avoids giant stack traces while pointing to the right name.
  • The heuristic considers every configured server (including ad-hoc ones registered via --http-url/--stdio). Tests covering this behaviour live in tests/cli-list.test.ts.
  • mcporter auth shares the same routing logic, so mcporter auth https://mcp.example.com/mcp (or even mcporter auth vercek) will spin up the temporary definition, auto-correct close names, and launch OAuth without touching the config file.

File: docs/call-syntax.md


summary: 'Reference for mcporter call argument styles, CLI signatures, and structured output modes.'
read_when:

  • 'Working on call CLI UX or documenting invocation examples'

Call Syntax Reference

mcporter call now understands two complementary styles:

Style Example Notes
Flag-based (compatible) mcporter call linear.create_comment --issue-id LNR-123 --body "Hi" Use --key value, --key=value, key=value, key:value, or key: value pairsโ€”ideal for shell scripts.
Function-call (expressive) mcporter call 'linear.create_comment(issueId: "LNR-123", body: "Hi")' Mirrors the pseudo-TypeScript signature shown by mcporter list; unlabeled values map to schema order.
Structured output mcporter call 'linear.create_comment(...)' --output json Successful calls emit JSON bodies; failures emit { server, tool, issue } envelopes so automation can react to auth/offline/http errors.

Both forms share the same validation pipeline, so required parameters, enums, and formats behave identically.

Reading the CLI Signatures

mcporter list <server> prints each tool as a compact TypeScript declaration:

ts
/**
 * Create a comment on a specific Linear issue.
 * @param issueId The issue ID
 * @param body The content of the comment as Markdown
 * @param parentId? A parent comment ID to reply to
 */
function create_comment(issueId: string, body: string, parentId?: string): Comment;
// optional (3): notifySubscribers, labelIds, mentionIds, ...

Key details:

  • Doc blocks use @param lines so every parameter description (even optional ones) stays in view.
  • Required parameters appear without ?; optional parameters use ? and inherit enum literals (e.g. "json" | "markdown").
  • Known format hints are appended inline: dueDate?: string /* ISO 8601 */ (we suppress the hint when the description already calls it out).
  • When a tool exposes more than two optional parameters (or has โ‰ฅ4 required parameters), the default output hides the extras and replaces them with an inline summary like // optional (8): limit, before, after, orderBy, projectId, ....
  • Run mcporter list <server> --all-parameters whenever you want the full signature; the footer repeats Optional parameters hidden; run with --all-parameters to view all fields. any time truncation occurs.
  • Return types come from each toolโ€™s output schema, so youโ€™ll see concrete names when providers include title metadata (e.g. DocumentConnection). When no schema is advertised we omit the : Type suffix entirely instead of showing unknown.
  • Each server concludes with a short Examples: block that mirrors the preferred function-call syntax.
  • Run mcporter list <server> --brief (or --signatures) when you only want the compact signatures and optional summaries without doc blocks or examples.

Function-Call Syntax Details

  • Named arguments preferred: issueId: "123" keeps calls self-documenting. When labels are omitted, mcporter falls back to positional order defined by the tool schema.
  • Optional positional fallback: omit labels when calling mcporter 'context7.resolve-library-id("React hooks docs", "react")'โ€”arguments map to the schema order after any explicitly named parameters.
  • Literals supported: strings, numbers, booleans, null, arrays, and nested objects. For strings containing spaces or commas, wrap the entire call in single quotes to keep the shell happy.
  • Error feedback: invalid keys, unsupported expressions, or parser failures bubble up with actionable messages (Unsupported argument expression: Identifier, Unable to parse call expression: โ€ฆ).
  • Server selection: You can embed the server in the expression (linear.create_comment(...)) or pass it separately (--server linear create_comment(...)).

HTTP Selectors & Ad-hoc URLs

  • You can skip --server entirely by pasting the MCP endpoint + tool name directly: mcporter call 'https://www.shadcn.io/api/mcp.getComponent(component: "vortex")'.
  • Mcporter strips the .tool suffix to derive the base server URL, reuses any configured definition that already points at that endpoint, and only registers a new ad-hoc server when necessary (--allow-http still guards plain HTTP URLs).
  • Function-call arguments continue to work in this form; if you omit parentheses you can still append key=value pairs (https://.../mcp.getComponent component=vortex).
  • This is especially handy for copy/pasting tool listings or experimenting with anonymous HTTP MCP serversโ€”you get the same parsing, auto-correction, and logging pipeline as the regular server.tool syntax.
  • Protocols are optional; bare domains such as shadcn.io/api/mcp.getComponents automatically assume https://. If you really need plain HTTP, spell out http:// and pass --allow-http.
  • Hostname comparisons ignore a leading www. so https://shadcn.io/api/mcp and https://www.shadcn.io/api/mcp resolve to the same configured server when possible.

Tips

  • Use --args '{ "issueId": "LNR-123" }', --params '{ "issueId": "LNR-123" }', --json '{ "issueId": "LNR-123" }', or --json - if you already have JSON payloads.
  • The new syntax respects all existing features (timeouts, --output, auto-correction).
  • Required fields show by default; pass --all-parameters when you want the full parameter list (or --schema for raw JSON schemas).
  • When in doubt, run mcporter list <server> to see the current signature and sample invocation.

Flag-Based Syntax Details

  • --key value, --key=value, key=value, key:value, key: value, and key:=value all map to the same named-argument handling, so you can type whichever feels most natural for your shell. Long flag keys convert kebab-case to camelCase (--save-to-drafts true becomes saveToDrafts: true). The := form is accepted as a compatibility alias for =.
  • By default, arguments keep the same validation pipeline as the function-call syntaxโ€”enums, numbers, and booleans are coerced automatically, and missing required fields raise errors.
  • --args -, --params -, and --json - read a JSON object from stdin.
  • Named flag-style values can read exact UTF-8 text from a file with key=@path or --key @path. Paths resolve from the current working directory, file contents remain strings without coercion, and key=@@literal produces the literal value @literal. Function-call strings such as body: "@literal" remain literal.
  • Bare string values supplied via long flags wrap into one-item arrays when the tool schema declares that field as an array.
  • Numeric-looking key=value arguments are restored to their original string spelling when the tool schema declares that parameter as a string, which keeps timestamp-like IDs such as Slack thread_ts=1234567890.123456 intact.
  • --raw-strings disables numeric coercion for flag-style and positional values so IDs/codes stay literal strings (code=12345 stays "12345").
  • --no-coerce disables all coercion for flag-style and positional values (true, null, and JSON-like values remain strings).
  • Long tool flags are checked against the selected tool schema before dispatch; undeclared flags fail with guidance to use key=value or --args. Insert -- before literal positional values that start with --.
  • --save-images <dir> keeps stdout formatting untouched while writing image content blocks to disk when a tool response includes type: "image" entries.
  • tool=value/tool:value and server=value still act as aliases for --tool / --server when you need to override the selector.

File: docs/cli-generator.md


summary: 'Behavior and architecture of mcporter generate-cli, including outputs, runtimes, regeneration, and policy boundaries.'
read_when:

  • 'Changing generate-cli behavior or bundler integrations'

CLI Generator

mcporter generate-cli produces a standalone CLI for one MCP server. Tool schemas become subcommands and schema fields become positional arguments or flags. By default, the command writes <server>.ts in the working directory. Bundling is opt-in via --bundle; Rolldown handles Node.js output, while Bunโ€™s native bundler handles Bun output. Use --bundler to override that choice.

Notes

  • Generated CLI depends on the latest commander for argument parsing.
  • Default timeout for tool calls is 30 seconds, overridable via --timeout.
  • Runtime flag remains (--runtime bun) to tailor shebang/usage instructions, but Node.js is the default.
  • Generated CLI embeds the resolved server definition and always targets that snapshot (no external --config or --server overrides at runtime).

Usage Examples

bash
# Minimal: infer the name from the command URL and emit TypeScript (optionally bundle)
npx mcporter generate-cli \
  --command https://mcp.context7.com/mcp \
  --minify

# Provide explicit name/description and compile a Bun binary (falls back to Node if Bun missing)
npx mcporter generate-cli \
  --name context7 \
  --command https://mcp.context7.com/mcp \
  --description "Context7 docs MCP" \
  --runtime bun \
  --compile

chmod +x context7
./context7
  # show the embedded help + tool list

# Shareable "one weird trick" for chrome-devtools (no config required)
npx mcporter generate-cli --command "npx -y chrome-devtools-mcp@latest"
  • --minify shrinks the bundled output via the selected bundler (output defaults to <server>.js).
  • --compile [path] implies bundling and invokes bun build --compile to create the native executable (Bun only). When you omit the path, the compiled binary inherits the server name.
  • Use --server '{...}' when you need advanced configuration (headers, env vars, stdio commands, OAuth metadata).
  • Omit --name to let mcporter infer it from the command URL (for example, https://mcp.context7.com/mcp becomes context7).
  • When targeting an existing config entry, you can skip --server and pass the name as a positional argument:
    npx mcporter generate-cli linear --bundle dist/linear.js.
  • When the MCP server is a stdio command, you can also skip --command by quoting the inline command as the first positional argument (e.g., npx mcporter generate-cli "npx -y chrome-devtools-mcp@latest").
  • Generated CLIs preserve lifecycle: "keep-alive" for embedded stdio servers. At runtime they create a stable generated config under ~/.mcporter/generated/ (or $XDG_STATE_HOME/mcporter/generated/ when set), auto-start the daemon as needed, and keep the server process alive across separate generated-CLI invocations.
  • Narrow the CLI to a specific subset of tools with --include-tools:
    npx mcporter generate-cli linear --include-tools issues_list,issues_create.
  • Hide debug or admin tools with --exclude-tools:
    npx mcporter generate-cli linear --exclude-tools debug_tool,admin_reset.

Artifact Metadata & Regeneration

  • Every generated artifact embeds its metadata (generator version, resolved server definition, invocation flags). A hidden __mcporter_inspect subcommand prints the payload without contacting the MCP server, so binaries remain self-describing even after being copied to another machine.
  • mcporter inspect-cli <artifact> shells out to that embedded command and prints a human summary (pass --json for raw output). The summary includes a ready-to-run generate-cli command you can reuse directly.
  • mcporter generate-cli --from <artifact> replays the stored invocation against the latest mcporter build. --server, --runtime, --timeout, --minify/--no-minify, --bundle, --compile, --output, and --dry-run let you override specific pieces of the stored metadata when necessary.
  • Because the metadata lives inside the artifact, any template, bundle, or compiled binary can be refreshed after a generator upgrade without juggling sidecar files.

Policy boundary for generated CLIs

A generated CLI or typed client can be invoked independently of the MCP client
that originally configured the server. It therefore does not automatically
inherit that client's approval prompts, tool-call policies, or audit trail.

Use --include-tools to reduce the generated surface, but do not treat a static
tool list as dynamic authorization. Deployments that need per-call policy should
route the generated command through an external wrapper or policy gateway that:

  1. normalizes the server, tool name, and arguments;
  2. evaluates current policy and requests approval when required;
  3. blocks the call or invokes the generated CLI; and
  4. persists a redacted decision and result record.

Conceptual wrapper pseudocode (not a built-in mcporter API):

ts
const operation = normalize({ server, tool, arguments });
let decision;
let outcome = 'evaluation_failed';
let result;
let failure;

try {
  decision = await gateway.evaluate(operation);

  if (decision.action === 'block') {
    outcome = 'blocked';
    failure = 'policy_block';
    throw new Error(decision.reason);
  }

  if (decision.action === 'approve' && !(await approvals.confirm(decision))) {
    outcome = 'approval_declined';
    failure = 'approval_declined';
    throw new Error('Approval declined');
  }

  try {
    result = await generatedCli.call(tool, arguments);
    outcome = 'succeeded';
    return result;
  } catch (error) {
    outcome = 'execution_failed';
    failure = classifyError(error);
    throw error;
  }
} catch (error) {
  failure ??= classifyError(error);
  throw error;
} finally {
  await audit.append(redact({ operation, decision, outcome, result, failure }));
}

The finally path records a redacted outcome for blocked requests, declined
approvals, policy-evaluation failures, execution failures, and successful calls.
The wrapper governs only calls routed through it. Direct execution of the
generated artifact bypasses that policy boundary.


File: docs/cli-reference.md


summary: 'Quick reference for mcporter subcommands, their arguments, and shared global flags.'
read_when:

  • 'Need a refresher on available CLI commands'

mcporter CLI Reference

A quick reference for the primary mcporter subcommands. Each command inherits
--config <file> and --root <dir> to override where servers are loaded from.

`mcporter list [server]`

  • Without arguments, lists every configured server (with live discovery + brief
    status).
  • With a server name, prints TypeScript-style signatures for each tool, doc
    comments, optional summaries, and any server instructions returned during
    MCP initialization.
  • With server.tool, prints just that tool; combine with --schema for a single
    tool schema.
  • Add --brief or --signatures with a server or server.tool target to keep
    the server header/instructions and print compact signatures without doc
    comments, examples, or schemas.
  • Add --status with a server target to print only the concise status row
    instead of full tool docs.
  • Add --exit-code to make the command exit 1 when any checked server is
    unhealthy, or --quiet to suppress output and imply --exit-code.
  • Hidden alias: list-tools (kept for muscle memory; not advertised in help output).
  • Hidden ad-hoc flag aliases: --sse for --http-url, --insecure for --allow-http (for plain HTTP testing).
  • Flags:
    • --brief โ€“ compact single-server output; cannot be combined with --json,
      --schema, --verbose, or --all-parameters.
    • --signatures โ€“ alias for --brief.
    • --all-parameters โ€“ include every optional parameter in the signature.
    • --schema โ€“ pretty-print the JSON schema for each tool.
    • --status โ€“ check server status only; cannot be combined with --brief,
      --schema, or --all-parameters.
    • --exit-code โ€“ exit 1 when any checked server is unhealthy.
    • --quiet โ€“ suppress output and exit 1 when any checked server is unhealthy.
    • --timeout <ms> โ€“ per-server timeout when enumerating all servers.
    • --no-oauth โ€“ never start an interactive OAuth flow; use cached
      tokens only while keeping eligible connections pooled.

`mcporter call `

  • Invokes a tool once and prints the response; supports positional arguments via
    pseudo-TS syntax and --arg flags.
  • Useful flags:
    • --server, --tool โ€“ alternate way to target a tool.
    • --args <json>, --params <json> โ€“ provide a JSON object payload.
    • -- โ€“ stop flag parsing so remaining tokens stay literal positional values.
    • --timeout <ms> โ€“ override the 60 s call timeout; MCPORTER_CALL_TIMEOUT provides the equivalent environment override.
    • --output text|markdown|json|raw โ€“ choose how to render the CallResult.
    • --save-images <dir> โ€“ persist image content blocks to files under the specified directory.
    • --raw-strings โ€“ disable numeric coercion for flag-style and positional values.
    • --no-coerce โ€“ disable all flag-style/positional value coercion.
    • key=@path / --key @path โ€“ read a named UTF-8 string argument from a file; prefix with @@ for a literal leading @.
    • Generic long tool flags are validated against the selected tool schema before dispatch.
    • --tail-log โ€“ stream tail output when the tool returns log handles.
    • --no-oauth โ€“ never start an interactive OAuth flow; use cached
      tokens only while keeping eligible connections pooled.

`mcporter resource [uri]`

  • Lists resources exposed by a server when no URI is provided.
  • Reads an MCP resource when uri is provided and renders text, markdown, JSON,
    or raw output using the same output formatter as mcporter call.
  • Hidden alias: resources.
  • Useful flags:
    • --output auto|text|markdown|json|raw โ€“ choose how to render the response.
    • --json โ€“ shortcut for --output json.
    • --raw โ€“ shortcut for --output raw.
    • --no-oauth โ€“ never start an interactive OAuth flow; use cached
      tokens only while keeping eligible connections pooled.

`mcporter serve [--servers a,b,c] [--stdio | --http ]`

  • Exposes daemon-managed keep-alive servers as one MCP server for clients that
    consume MCP over stdio or Streamable HTTP.
  • tools/list queries the daemon for each selected server and publishes tools
    as server__tool; tools/call strips the prefix and routes the call through
    the daemon.
  • In HTTP mode, /mcp keeps the aggregate namespaced bridge, while
    /mcp/<server> exposes one selected keep-alive server with its original,
    unprefixed tool names.
  • Only configured keep-alive servers participate. Add
    "lifecycle": "keep-alive" to a server definition when you want it managed
    by the daemon.
  • Flags:
    • --stdio โ€“ serve MCP over stdio; this is the default and is the mode to
      register with Claude Code, Codex, and similar clients.
    • --http โ€“ serve MCP Streamable HTTP on /mcp and
      /mcp/<server>, bound to 127.0.0.1 by default.
    • --host <host> โ€“ override the HTTP bind host when you intentionally need a
      non-local listener.
    • --servers <csv> โ€“ expose only the listed keep-alive server names.

`mcporter generate-cli`

  • Produces a standalone CLI for a single MCP server (optionally bundling or
    compiling with Bun).
  • Key flags:
    • --server <name> (or inline JSON) โ€“ choose the server definition.
    • --command โ€“ point at an ad-hoc HTTP endpoint (include https:// or use host/path.tool) or a stdio command (anything with spaces, e.g., "npx -y chrome-devtools-mcp@latest"). If you omit --command, the first positional argument is inspected: whitespace โ†’ stdio, otherwise the parser probes for HTTP/HTTPS and falls back to config names.
    • --output โ€“ where to write the TypeScript template.
    • --bundle โ€“ emit a bundle (Node/Bun) ready for bun x.
    • --bundler rolldown|bun โ€“ pick the bundler implementation (defaults to Rolldown unless the runtime resolves to Bun, in which case Bunโ€™s bundler is used automatically; still requires a local Bun install).
    • --compile โ€“ compile with Bun (implies --runtime bun).
    • --include-tools <a,b,c> โ€“ only generate subcommands for the specified tools (exact MCP tool names). It is an error if any requested tool is missing.
    • --exclude-tools <a,b,c> โ€“ generate subcommands for every tool except the ones listed. Unknown tool names are ignored. If all tools are excluded, generation fails.
    • --include-tools and --exclude-tools are mutually exclusive.
    • --timeout <ms> / --runtime node|bun โ€“ shared via the generator flag
      parser so defaults stay consistent.
    • --from <artifact> โ€“ reuse metadata from an existing CLI artifact (legacy
      regenerate-cli behavior, must point at an existing CLI).
    • --dry-run โ€“ print the resolved mcporter generate-cli ... command without
      executing (requires --from).
    • Positional shorthand: npx mcporter generate-cli linear uses the configured
      linear definition; npx mcporter generate-cli https://example.com/mcp
      treats the URL as an ad-hoc server definition.

`mcporter emit-ts `

  • Emits TypeScript definitions (and optionally a ready-to-use client) describing
    a serverโ€™s tools. This reuses the same formatter as mcporter list so doc
    comments, signatures, and examples stay in sync.
  • Modes:
    • --mode types --out <file.d.ts> (default) โ€“ export an interface whose
      methods return Promise<CallResult>, with doc comments and optional
      summaries.
    • --mode client --out <file.ts> โ€“ emit both the interface (<file>.d.ts)
      and a factory that wraps createServerProxy, returning objects whose
      methods resolve to CallResult.
  • Other flags:
    • --include-optional (alias --all-parameters) โ€“ show every optional field.
    • --types-out <file> โ€“ override where the .d.ts sits when using client
      mode.

For more detail about configuration and OAuth flows, see docs/config.md and
the command-specific docs under docs/.


File: docs/config.md


summary: 'How mcporter discovers, merges, and mutates configuration files (project + home + imports), including OAuth and persistence.'
read_when:

  • 'Working on config resolution, imports, or mcporter config subcommands'

Configuration Guide

Overview

mcporter keeps three configuration buckets in sync: repository-scoped JSON (config/mcporter.json), imported editor configs (Cursor, Claude, Codex, Windsurf, OpenCode, VS Code), and ad-hoc definitions supplied on the CLI. This guide explains how those sources merge, how to mutate them with mcporter config ..., and the safety rails around OAuth, env interpolation, and persistence.

Quick Start

  1. Create config/mcporter.json at the repo root:
    jsonc
    {
      "$schema": "https://raw.githubusercontent.com/openclaw/mcporter/main/mcporter.schema.json",
      "mcpServers": {
        "linear": {
          "description": "Linear issues",
          "baseUrl": "https://mcp.linear.app/mcp",
          "headers": { "Authorization": "Bearer ${LINEAR_API_KEY}" },
        },
      },
      "imports": ["cursor", "claude-code", "claude-desktop", "codex", "windsurf", "opencode", "vscode"],
    }
    The $schema property enables IDE autocomplete and validation. Use the raw GitHub URL for the latest schema, or copy mcporter.schema.json locally.
  2. Run mcporter list linear (or mcporter config list linear) to make sure the runtime can reach it.
  3. Use mcporter config add shadcn https://www.shadcn.io/api/mcp to persist another server without editing JSON.
  4. Authenticate any OAuth-backed server with either mcporter auth <name> or mcporter config login <name>; tokens land in the shared vault (~/.mcporter/credentials.json, or $XDG_DATA_HOME/mcporter/credentials.json when set) unless you override tokenCacheDir.

Config Resolution Order

mcporter now merges home and project config files by default so global servers stay available inside repos. The order depends on how you invoke the CLI:

  1. If you pass --config <file> (or set --config programmatically), only that file is usedโ€”no merging.
  2. If MCPORTER_CONFIG is set, only that file is usedโ€”no merging.
  3. Otherwise, mcporter loads both of these layers (when present):
    • $XDG_CONFIG_HOME/mcporter/mcporter.json[c] when XDG_CONFIG_HOME is set, falling back to ~/.mcporter/mcporter.json[c] when no XDG mcporter config exists
    • <root>/config/mcporter.json
      Entries from the project file override entries with the same name from the home file. Each layer still pulls in its own imports before merging.

All mcporter config โ€ฆ mutations still write back to a single file: the explicit path when provided; otherwise the project config path (<root>/config/mcporter.json). To edit the home file explicitly, run commands like mcporter config --config ~/.mcporter/mcporter.json add <name> โ€ฆ or set MCPORTER_CONFIG in your shell profile.

mcporter honors XDG Base Directory env vars for its own paths when they are explicitly set to absolute paths:

Kind Env var mcporter path Legacy fallback
config XDG_CONFIG_HOME $XDG_CONFIG_HOME/mcporter/mcporter.json[c] ~/.mcporter/...
data XDG_DATA_HOME $XDG_DATA_HOME/mcporter/credentials.json ~/.mcporter/...
cache XDG_CACHE_HOME $XDG_CACHE_HOME/mcporter/<server>/schema.json ~/.mcporter/...
state XDG_STATE_HOME $XDG_STATE_HOME/mcporter/daemon/... ~/.mcporter/daemon

Unset, empty, or relative XDG vars fall back to ~/.mcporter for backwards compatibility. For config files only, an absolute XDG_CONFIG_HOME is XDG-first but still probes ~/.mcporter/mcporter.json[c] when no XDG mcporter config exists, so embedders that sandbox unrelated tools with XDG_CONFIG_HOME do not accidentally hide the user's registry. Explicit overrides still win: --config/MCPORTER_CONFIG for config files, tokenCacheDir for per-server OAuth/schema cache directories, and MCPORTER_DAEMON_DIR for daemon files.

Discovery & Precedence

mcporter builds a merged view of all known servers before executing any command. The sources load in this order:

Priority Source Notes
1 Explicit --http-url, --stdio, or bare URL passed to commands Highest priority, never cached unless --persist is supplied. Requires --allow-http for plain HTTP URLs.
2 config/mcporter.json (or the file passed via --config) Default path is <root>/config/mcporter.json; missing file returns an empty config so commands continue to work.
3 Imports listed in "imports" When you omit imports, mcporter loads ['cursor','claude-code','claude-desktop','codex','windsurf','opencode','vscode']. When you specify a non-empty array, mcporter appends any omitted defaults after your list so shared presets remain available.

Rules:

  • Later sources never override earlier ones. Local config always wins over imports; ad-hoc descriptors override both for the duration of a command.
  • Each merged server tracks its origin (local path vs. import path), so mcporter config get <name> can show you the path before you edit or remove the local copy with mcporter config remove <name>.
  • Imports remain read-only until you explicitly copy an entry via mcporter config import <kind> --copy or run mcporter config add --copy-from claude-code:linear.

CLI Workflows

mcporter config is the entry point for reading and writing configuration files. Use the existing ad-hoc flags on mcporter list|call|auth when you want ephemeral definitions; once youโ€™re ready to persist them, switch back to mcporter config add.

Use --scope home|project with mcporter config add to pick the write target explicitly. project is always the default (creating config/mcporter.json if needed); home writes to the XDG config path when XDG_CONFIG_HOME is set, otherwise ~/.mcporter/mcporter.json, even when a project config is present. --persist still takes precedence when you need a custom file.

`mcporter config list [filter]`

  • Shows local entries by default. Pass --source import to list imported editor configs, or --json for machine output.
  • Always appends a summary of other config files (paths, counts, sample names) so you know where imported entries live.
  • filter accepts a name, glob fragment, or source:cursor selector.
  • Adds informational notes when we auto-correct names (same machinery as mcporter list).

`mcporter config get `

  • Prints the resolved definition for a single server, including the on-disk path, inherited headers/env, and transport details.
  • Near-miss names are auto-corrected with the same heuristics as mcporter list/call, and youโ€™ll see suggestions whenever ambiguity remains.
  • Supports ad-hoc descriptors so you can inspect a URL before persisting it.

`mcporter config add [target]`

  • Persists a server into the writable config file. Accepts both positional shortcuts (mcporter config add sentry https://mcp.sentry.dev/mcp) and flag-driven definitions:
    • --transport http|sse|stdio
    • --url or --command/--stdio
    • --env, --header, --token-cache-dir, --description, --client-name, --oauth-redirect-url
    • --oauth-client-id, --oauth-client-secret-env, --oauth-token-endpoint-auth-method for pre-registered OAuth clients.
    • --copy-from importKind:name to clone settings from an imported entry before editing.
  • --dry-run shows the JSON diff without writing, while --persist overrides the destination file.

`mcporter config remove `

  • Removes the local definition. Names sourced exclusively from imports remain untouched until you copy them locally.

`mcporter config import `

  • Displays (and optionally copies) entries from editor-specific configs:
    • cursor: .cursor/mcp.json in the repo, falling back to ~/.config/Cursor/User/mcp.json (or %APPDATA%/Cursor/User on Windows).
    • claude-code: <root>/.claude/settings.local.json, <root>/.claude/settings.json, <root>/.claude/mcp.json, then ~/.claude/settings.json, ~/.claude/mcp.json, ~/.claude.json. settings.local.json is meant for untracked per-developer overrides, while settings.json is the shared project config.
    • claude-desktop: platform-specific Claude/claude_desktop_config.json paths.
    • codex: <root>/.codex/config.toml, then ~/.codex/config.toml.
    • windsurf: Codeiumโ€™s Windsurf config under %APPDATA%/Codeium/windsurf/mcp_config.json or ~/.codeium/windsurf/mcp_config.json.
    • opencode: Honors OPENCODE_CONFIG when set, then <root>/opencode.json(c), OPENCODE_CONFIG_DIR/opencode.json(c), and finally ${XDG_CONFIG_HOME:-~/.config}/opencode/opencode.json(c) (or %APPDATA%/opencode/opencode.json(c) on Windows). Both .json and .jsonc extensions are supported.
    • vscode: Code/User/mcp.json (stable + Insiders) inside the OS-appropriate config directory.
  • --copy writes selected entries into your local config; --filter <glob> narrows the import list; --path <file> lets you point at bespoke locations.

`mcporter config login ` / `logout`

  • Mirrors mcporter auth. login completes OAuth (or token provisioning) for either a named server or an ad-hoc URL. When a hosted MCP returns 401/403, mcporter automatically promotes that target to OAuth and re-runs the flow, matching the behavior documented in docs/adhoc.md.
  • --no-browser suppresses automatic browser launch and prints the authorization URL to stdout so it can be copied from a headless host. --browser none is accepted as a compatibility alias, and MCPORTER_OAUTH_NO_BROWSER=1 / true / yes enables the same behavior by environment.
  • In --json --no-browser mode, stdout contains a JSON object with authorizationUrl and redirectUrl; diagnostics stay off stdout so scripts can parse the result. Treat emitted authorization URLs as sensitive operational output.
  • A configured server name is an OAuth trust boundary. Changing its URL clears cached tokens, dynamic client registration, PKCE verifier, and callback state from the shared vault and any tokenCacheDir; changing the URL back does not restore the old credentials, so re-authentication is required.
  • logout wipes the shared vault entry, legacy ~/.mcporter/<name>/ caches, and the custom tokenCacheDir when present. Pass --all to clear everything.

`mcporter config doctor`

  • Early validator that checks for simple issues (e.g., OAuth entries missing cache paths). Future iterations will add fixes for Accept headers, duplicate imports, and more.

Ad-hoc & Persistence

  • --http-url and --stdio flags live on mcporter list|call|auth, keeping mcporter config focused on persistent config files. Ad-hoc HTTP targets also accept repeatable --header KEY=value flags for private endpoints.
  • Names default to slugified hostnames or executable/script combos. Supply --name to improve reuse; mcporter uses that slug for OAuth caches even before persistence.
  • --allow-http is mandatory for cleartext endpoints so we never downgrade transport silently.
  • Add --persist (defaulting to config/mcporter.json when omitted) to copy the ad-hoc definition into config. We reuse the same serializer as the import pipeline, so copying from Cursor โ†’ local config produces identical structure and preserves custom env/header fields.
  • When an ad-hoc HTTP server returns an OAuth challenge during list, call, or auth, the persisted entry is rewritten with auth: "oauth" so later commands use the cached OAuth path instead of retrying unauthenticated HTTP.
  • --env KEY=VAL entries merge with existing env dictionaries if you later persist the same server; nothing is lost when you alternate between CLI flags and JSON edits.
  • --header KEY=VAL entries merge into the persisted HTTP headers object when used with --persist; values support the same $env:VAR, ${VAR}, and ${VAR:-fallback} placeholders as config-file headers.

HTTP Compatibility

HTTP MCP servers normally use Node's built-in fetch for request/response traffic. MCPorter's optional long-lived SSE receive channel uses a separate HTTP/1.1 connection so an idle stream cannot occupy the request pool and stall later tool calls. Set httpFetch: "default" to opt out of that isolation and use the built-in fetch for every request.

If a provider rejects the built-in stack entirely but accepts plain Node https.request traffic, set httpFetch: "node-http1" on the server entry to force an HTTP/1.1 fetch implementation for all Streamable HTTP and SSE requests:

jsonc
{
  "mcpServers": {
    "sunsama": {
      "baseUrl": "https://api.sunsama.com/mcp",
      "headers": { "Authorization": "Bearer ${SUNSAMA_TOKEN}" },
      "httpFetch": "node-http1",
    },
  },
}

The Sunsama endpoint is auto-detected and uses the all-request compatibility path by default.

JSON Schema for IDE Support

mcporter provides a JSON Schema for config file validation and autocompletion. Add the $schema property to your config file:

jsonc
{
  "$schema": "https://raw.githubusercontent.com/openclaw/mcporter/main/mcporter.schema.json",
  "mcpServers": { ... }
}

For local development, you can reference the schema from the repo root:

jsonc
{
  "$schema": "../mcporter.schema.json",
  "mcpServers": { ... }
}

The schema is auto-generated from the Zod validation schemas using pnpm generate:schema.

Schema Reference

Top-level structure:

Key Type Description
mcpServers object Map of server names โ†’ definitions. Required even if empty.
imports string[] Optional list of import kinds. Empty array disables imports entirely; omitting the key falls back to the default list.

Server definition fields (subset of what RawEntrySchema accepts):

Field Description
description Free-form summary printed by mcporter list/config list.
baseUrl / url / serverUrl HTTPS or HTTP endpoint. http:// requires --allow-http in ad-hoc mode but works in config if you explicitly set it.
command / args Stdio executable definition (string or array). Arrays are preferred because they avoid shell quoting issues.
cwd Working directory for stdio servers. A leading ~ is expanded to $HOME; relative paths resolve against the config file directory. Defaults to the config file directory when omitted.
env Key/value pairs applied when launching stdio commands. Supports ${VAR} interpolation and ${VAR:-fallback} defaults. Existing process env values win over fallbacks.
headers Request headers for HTTP/SSE transports. Values can reference $env:VAR or ${VAR} placeholders, which must be set at runtime or mcporter aborts with a helpful error.
auth Recognizes oauth and refreshable_bearer. oauth runs the browser/client OAuth flow for HTTP transports; refreshable_bearer refreshes cached bearer tokens non-interactively before connecting.
tokenCacheDir Directory for OAuth tokens and schema caches; still honored, but mcporter now keeps a centralized vault in ~/.mcporter/credentials.json or $XDG_DATA_HOME/mcporter/credentials.json when set (legacy per-server caches are auto-migrated). Supports ~ expansion.
clientName Optional identifier some servers use for telemetry/audience segmentation.
protocolVersion / protocol_version Protocol negotiation override: auto (the default), legacy for byte-compatible 2025-era connections, or 2026-07-28 to require that modern revision.
chromeDevtoolsRelay / chrome_devtools_relay OpenClaw Chrome extension relay policy for eligible chrome-devtools-mcp --autoConnect stdio definitions: prefer (default), require, or off. MCPORTER_CHROME_DEVTOOLS_RELAY_POLICY overrides config. See the daemon guide.
oauthClientId Pre-registered OAuth client id for providers that do not support dynamic client registration.
oauthClientSecretEnv Environment variable containing the OAuth client secret. Prefer this over committing oauthClientSecret directly.
oauthTokenEndpointAuthMethod Optional token endpoint auth method override, for example client_secret_post when the provider requires client credentials in the token request body.
oauthRedirectUrl Override the default localhost callback. Required for many pre-registered OAuth apps because the provider must allowlist the exact redirect URI. Also useful when tunneling OAuth through Codespaces or remote dev boxes.
oauthClientMetadataUrl Optional HTTPS Client ID Metadata Document URL. When the authorization server supports URL-based client identity, the SDK uses this URL as the client id; otherwise it falls back to dynamic client registration.
oauthScope Optional explicit OAuth scope string. If omitted, mcporter lets the MCP SDK derive scope from server/auth metadata. Use this as an escape hatch for providers that require explicit scopes but donโ€™t publish scopes_supported.
oauthCommand.args For STDIO servers that ship a custom auth subcommand (e.g., Gmail MCP). mcporter will spawn the stdio command with these args when you run mcporter auth <name>, so you donโ€™t need to call npx ... auth manually.
refresh Explicit token refresh settings for auth: "refreshable_bearer". Supports tokenEndpoint, clientIdEnv, clientSecretEnv, clientAuthMethod, refreshSkewSeconds, and accessTokenEnv (plus snake_case aliases).
allowedTools / allowed_tools Optional exact-name allowlist. Only listed tools appear in mcporter list and can be called. An empty array blocks all tools. Cannot be combined with blockedTools.
blockedTools / blocked_tools Optional exact-name blocklist. Listed tools are hidden from mcporter list and rejected by mcporter call. Cannot be combined with allowedTools.

mcporter normalizes headers to include Accept: application/json, text/event-stream automatically, matching the runtimeโ€™s streaming expectations.
String-valued config fields support ${VAR} and ${VAR:-fallback} placeholders. Secret-bearing headers, env, and bearer-token placeholders are preserved in config get/config list output and resolved only when the transport runs; *Env fields name environment variables and are not expanded.

Top-level daemonIdleTimeoutMs (or daemon_idle_timeout_ms) shuts down the keep-alive daemon after that many milliseconds without daemon requests. Per-server lifecycle.idleTimeoutMs still controls when individual keep-alive transports are closed.

Refreshable Bearer Tokens

Use auth: "refreshable_bearer" when you already seeded OAuth tokens with mcporter vault set <server> or tokenCacheDir, and the server should receive only a fresh bearer token at runtime. HTTP servers get Authorization: Bearer <token> when no authorization header is already configured. STDIO servers require refresh.accessTokenEnv; mcporter refreshes before spawning the process and injects that env var with the raw access token.

json
{
  "mcpServers": {
    "example": {
      "command": "uvx",
      "args": ["example-mcp-server"],
      "auth": "refreshable_bearer",
      "refresh": {
        "tokenEndpoint": "https://api.example.com/oauth/token",
        "clientIdEnv": "EXAMPLE_CLIENT_ID",
        "clientSecretEnv": "EXAMPLE_CLIENT_SECRET",
        "clientAuthMethod": "client_secret_basic",
        "refreshSkewSeconds": 300,
        "accessTokenEnv": "EXAMPLE_ACCESS_TOKEN"
      }
    }
  }
}

For keep-alive stdio servers, refresh happens before process start. If that process cannot read updated credentials after startup, use lifecycle: "ephemeral" or restart the daemon before the injected token expires.

Imports & Conflict Resolution

  • pathsForImport(kind, rootDir) determines every candidate path. mcporter searches the repo first, then user-level directories, and stops at the first file that parses.
  • Entries pulled from imports are treated as read-only snapshots. The merge process keeps the first definition for each name; later sources with the same name are skipped until you override locally.
  • To copy an imported entry, either run mcporter config import <kind> --copy --filter name or use mcporter config add name --copy-from kind:name. The copy operation writes through the same JSON normalization stack, so the resulting file matches our schema even if the source format was TOML (Codex) or legacy JSON shapes (servers vs mcpServers).

Project vs. Machine Layers

  • Keep config/mcporter.json under version control. Encourage contributors to add sensitive data via env vars (${LINEAR_API_KEY}) rather than inline secrets.
  • For pre-registered OAuth apps, store the public oauthClientId in config and point oauthClientSecretEnv at a local environment variable. oauthClientSecret is supported for private machine-local configs but should not be committed.
  • For headless deployments that already have OAuth credentials, run mcporter vault set <server> --tokens-file or mcporter vault set <server> --stdin with a JSON payload shaped like { "tokens": { ... }, "clientInfo": { ... } }. This lets mcporter compute the vault key from the resolved server definition instead of duplicating that internal format in scripts.
  • Machine-specific additions can live in ~/.mcporter/local.json or $XDG_CONFIG_HOME/mcporter/local.json; point mcporter config --config ~/.mcporter/local.json add ... there when you prefer not to touch the repo. Since the runtime only watches one config at a time, CI jobs should always pass --config config/mcporter.json (or run from the repo root) for deterministic behavior.
  • OAuth tokens, cached server metadata, and generated CLIs should remain outside the repo (~/.mcporter/... or the matching XDG_*_HOME/mcporter/..., plus dist/).

Validation & Troubleshooting

  • mcporter list --http-url ... refuses to auto-run OAuth to keep listing commands quick; use mcporter config login ... or mcporter auth ... to finish credential setup.
  • When env placeholders are missing, commands fail fast with the exact variable name. Add the variable or wrap it in ${VAR:-fallback} to provide defaults.
  • Use mcporter config get <name> to confirm where a server came from. If a teammateโ€™s Cursor config keeps overriding your local entry, reorder the imports array to move Cursor later or set it to [] to disable imports entirely.
  • docs/adhoc.md covers deeper debugging, including tmux workflows and OAuth promotion logs.

File: docs/daemon.md


summary: 'How mcporter keeps stateful MCP servers warm and exposes them through its bridge.'
read_when:

  • 'Configuring keep-alive servers, the daemon, serve mode, or Chrome DevTools'

Keep-alive daemon

MCPorter can keep stateful stdio servers alive across separate CLI invocations. A per-login daemon owns those transports, while the ordinary mcporter list and mcporter call commands route eligible servers through its local socket.

Lifecycle selection

Chrome DevTools, Mobile MCP, Playwright MCP, and CloudBase MCP definitions default to keep-alive when MCPorter recognizes their configured name or stdio command. Other servers remain ephemeral unless their config opts in:

jsonc
{
  "mcpServers": {
    "browser": {
      "command": "npx",
      "args": ["-y", "chrome-devtools-mcp@latest", "--autoConnect"],
      "lifecycle": "keep-alive",
    },
    "interactive": {
      "url": "https://example.com/mcp",
      "lifecycle": "ephemeral",
    },
  },
}

MCPORTER_KEEPALIVE=name opts named servers in. MCPORTER_DISABLE_KEEPALIVE=name opts them out; both variables accept comma-separated names or *. Explicit config remains the clearest choice for project-owned definitions.

Ad-hoc targets passed with --stdio, --http-url, or a URL selector are per-process. Persist the definition into config before expecting it to participate in the shared daemon.

Daemon commands

sh
mcporter daemon status
mcporter daemon start
mcporter daemon restart
mcporter daemon stop

Eligible servers start the daemon automatically when a list or call needs them. Use explicit commands to inspect, pre-warm, restart after configuration changes, or stop every managed transport.

Daemon calls are non-interactive. If a server requests elicitation, the call is declined with a hint to use an ephemeral terminal call instead. See protocols and interactive requests.

Set MCPORTER_OAUTH_NO_BROWSER=1 (also accepts true or yes) on a serve/daemon process to suppress automatic OAuth browser launches. Affected calls fail once with the server name and an mcporter auth <name> --no-browser hint instead of restarting or replaying. Changing the normalized setting replaces an already-running daemon on the next operation.

The local socket timeout is an idle liveness budget rather than a cap on the whole operation. A current daemon emits progress frames while work is in flight, allowing interactive OAuth to run to its own deadline without causing a daemon restart or a duplicate browser prompt. Mixed versions remain compatible: a current client falls back to the previous flat deadline with an older daemon, while a current daemon sends the single-response format expected by older clients unless the caller explicitly opts into progress frames.

Expose servers to another MCP client

mcporter serve turns the daemon's keep-alive servers back into an MCP server:

sh
mcporter serve --stdio
mcporter serve --http 3000
mcporter serve --http 3000 --servers chrome-devtools,playwright

The stdio form is suitable for clients such as Claude Code and Codex. HTTP mode binds to 127.0.0.1 by default and serves an aggregate endpoint at /mcp, with namespaced tools such as chrome-devtools__list_pages. It also serves /mcp/<server> with that server's original tool names.

Chrome DevTools through the OpenClaw extension relay

chrome-devtools-mcp --autoConnect uses Chrome's remote-debugging handshake, which can display an โ€œAllow remote debugging?โ€ dialog for each session. When the OpenClaw Chrome extension relay is paired on the same host, MCPorter can use that extension-backed Chrome control path instead. Automatic rewriting supports direct chrome-devtools-mcp commands and the standard npx/bunx launch forms used by MCPorter definitions. Other shell and package-manager wrappers remain unchanged under prefer; require rejects recognizable Chrome auto-connect commands behind those unsupported wrappers instead of silently launching the legacy path.

MCPorter requires an OpenClaw relay that implements Browser Relay Authentication v2, including the connection-bound challenge, completion, authenticated /json/version, and same-connection /cdp upgrade sequence. An older OpenClaw relay is unsupported: require fails with unsupported-auth, while prefer may launch the original Chrome --autoConnect path. MCPorter never retries an old Bearer, Basic, or raw-token relay handshake, even when the v2 endpoint returns 404, 401, or 426, a proof fails, or the handshake times out. During OpenClaw's dual-stack migration window, legacy acceptance exists only for older clients; current MCPorter always uses v2.

The mode-0600, current-user relay key never enters an HTTP header, URL, WebSocket subprotocol, application frame, child command line, or child environment. MCPorter derives its non-secret keyId, connects once to a numeric loopback address, verifies the connected peer, and keeps that exact raw TCP socket from the HMAC challenge through completion, /json/version, and the /cdp WebSocket upgrade. It does not follow redirects, reconnect, re-resolve, or hand the key to the proxy. Only after both server proofs verify does MCPorter start a short-lived downstream proxy bound strictly to 127.0.0.1; that proxy wraps the already-authenticated and already-upgraded socket rather than opening another upstream connection.

The same raw client supports the protocol's separate json-list flow: it authenticates one /json/list request on the retained socket, reads a bounded response, and closes instead of upgrading.

Each downstream proxy retains the protections introduced for the child handoff: it gets a fresh 256-bit authorization bearer, MCPorter writes that ephemeral value to an exclusively created mode-0600 file inside a mode-0700 temporary directory, and only the protected file path reaches a Node preload through the child environment. The preload validates and consumes the file, removes the handoff variable, and appends --wsHeaders only to JavaScript's process.argv; the OS command line contains only the credential-free loopback WebSocket endpoint. The proxy accepts exactly one authorized downstream WebSocket, synthesizes that child's 101 response, and then bridges frames to the retained upstream socket. Losing either side retires the proxy and its authenticated connection.

The preload composes with existing NODE_OPTIONS, including MCPorter's separate Chrome compatibility preload. The proxy, handoff file, preload, and temporary directory are closed or removed on normal shutdown, setup failure, abort, and negotiation retry. On POSIX systems, ownership and permissions are checked before the handoff is consumed. On Windows, MCPorter creates the temporary directory atomically with a verified current-user-only ACL before its path exists; setup fails closed if that security descriptor cannot be established. Plaintext relay URLs remain limited to loopback, including custom URLs set with MCPORTER_CHROME_DEVTOOLS_RELAY_URL; remote HTTP relay targets and URLs containing credentials are rejected.

Choose routing with chromeDevtoolsRelay (or chrome_devtools_relay) on the server definition, or override it for the process with MCPORTER_CHROME_DEVTOOLS_RELAY_POLICY:

  • prefer is the default. It uses the v2 extension relay when authentication and the protected child handoff succeed; otherwise it may keep only the original Chrome --autoConnect behavior. It never falls back to legacy relay authentication.
  • require fails before any legacy auto-connect process is launched if the endpoint, credential, probe, authentication, extension connection, local proxy, or protected handoff is unavailable. Retries remain fail-closed.
  • off disables relay probing and rewriting. The older MCPORTER_DISABLE_CHROME_DEVTOOLS_RELAY=1 switch remains an alias for off.
jsonc
{
  "mcpServers": {
    "chrome-devtools": {
      "command": "npx",
      "args": ["-y", "chrome-devtools-mcp@latest", "--autoConnect"],
      "lifecycle": "keep-alive",
      "chromeDevtoolsRelay": "require",
    },
  },
}

Before probing, MCPorter runs the non-secret openclaw browser extension cdp --json metadata command without the legacy-bearer option. POSIX launches openclaw directly. Windows resolves an existing openclaw.cmd only from safe absolute PATH entries, rejects the current directory and command-expanding path bytes, then passes the quoted absolute shim plus constant arguments to a validated system cmd.exe. Process spawning always keeps shell: false; time and combined output are bounded, and timeout or overflow terminates the full Windows process tree through the validated system taskkill.exe. Fallback diagnostics never include stdout or stderr. MCPorter accepts only the exact loopback Browser Relay Authentication v2 CDP contract whose keyId matches the local relay credential. An explicit nonempty MCPORTER_CHROME_DEVTOOLS_RELAY_URL remains authoritative and skips metadata discovery; unavailable, timed-out, malformed, unsafe, or incompatible metadata falls back to http://127.0.0.1:18799. Relay control and OpenClaw discovery variables in a server definition's env map use the same placeholder resolution as its runtime transport and override process-level values for both routing and daemon identity.

Metadata discovery and the authenticated /json/version probe each use the relay timeout, which is 5 seconds by default. MCPORTER_CHROME_DEVTOOLS_RELAY_TIMEOUT_MS accepts milliseconds, clamped to 100โ€“30000; unset, zero, non-integer, and otherwise invalid values use 5000. Credential discovery follows OpenClaw: OPENCLAW_OAUTH_DIR wins, then OPENCLAW_STATE_DIR; otherwise credentials live under the effective home in .openclaw/credentials, or .openclaw-/credentials for a validated non-default OPENCLAW_PROFILE. Explicit OPENCLAW_HOME takes precedence over HOME and USERPROFILE.

Keep-alive daemons include the effective policy, discovered or explicit URL, timeout, state directory, credential directory, v2 protocol marker, and derived keyId in their runtime identity. Both daemon metadata creation and freshness checks await the same bounded asynchronous resolver; freshness never launches OpenClaw through a synchronous child process. Changing the selected OpenClaw profile or relay port, or rotating the key at the same path, therefore replaces stale daemon state on the next operation without exposing key material; no manual restart is required. mcporter daemon status shows the current or last redacted decision with route, policy, reason, safe logical upstream endpoint, and probe status/duration. Set MCPORTER_LOG_LEVEL=info to see the same structured decision during ordinary connection setup. Diagnostics distinguish unsupported v2 authentication, bad server proofs, unauthenticated server failures, replay, protocol, freshness, and sequence failures, disconnected extensions (an authenticated 503), timeouts, network errors, protected handoff failures, and success. They never print keys, proofs, nonces, stable or ephemeral bearers, handoff paths, headers, pairing strings, or child arguments.

MCPorter also applies its Chrome DevTools auto-connect compatibility patch when relevant. Set MCPORTER_DISABLE_CHROME_DEVTOOLS_COMPAT=1 to disable that separate behavior.

Logging and shutdown

Use mcporter daemon start --log or mcporter daemon restart --log to keep a daemon log. --log-file chooses the destination, and --log-servers <csv> limits per-call traces. The logging guide covers environment and per-server controls.

The daemon closes managed transports on stop. A top-level daemonIdleTimeoutMs can shut down an inactive daemon, while a keep-alive lifecycle object can set an idle timeout for one server. See configuration for both forms.


File: docs/emit-ts.md


summary: 'How to generate .d.ts files or typed client helpers with mcporter emit-ts.'
read_when:

  • 'Adding new emit-ts behavior or troubleshooting generated clients'

`mcporter emit-ts`

mcporter emit-ts turns a configured MCP server into TypeScript artifacts so
agents, tests, and tooling can consume the server through strongly typed APIs.
It reuses the same buildToolDoc() data that powers mcporter list, so doc
comments, parameter hints, and signatures stay perfectly in sync. For a broader
overview of every CLI command, see docs/cli-reference.md.

text
mcporter emit-ts <server> --out linear-client.ts \
  [--mode types|client] \
  [--include-optional]
  • --mode types (default) emits a .d.ts interface (LinearTools) with
    docblocks + promisified signatures. Missing output schemas fall back to
    CallResult.
  • --mode client emits both the interface (auto-derived .d.ts) and an
    executable .ts helper that wraps createServerProxy. Each method returns a
    CallResult, and the factory exposes a close() helper for runtimes the client
    creates.
  • --include-optional mirrors mcporter list --all-parameters, ensuring every
    parameter is shown even when optional.

Outputs overwrite existing files automatically so you can regenerate artifacts
whenever the server schema changes.

Examples

1. Types-only header

text
mcporter emit-ts linear --out types/linear-tools.d.ts

Produces:

ts
import type { CallResult } from 'mcporter';

export interface LinearTools {
  /**
   * List comments for a specific Linear issue.
   *
   * @param issueId The issue ID
   */
  list_comments(params: { issueId: string }): Promise<CallResult>;
}

Include the file in your agent/project and you can type-check code like
const result = await proxy.list_comments({ issueId: '...' });.

2. Client wrappers

text
mcporter emit-ts linear --mode client --out clients/linear.ts

Generates two files:

  • clients/linear.d.ts โ€“ same interface as the types-only mode.
  • clients/linear.ts โ€“ imports createRuntime, createServerProxy, and
    createCallResult, then exposes a createLinearClient() factory:
ts
const client = await createLinearClient({ configPath: './mcporter.json' });
const comments = await client.list_comments({ issueId: 'LIN-1234' });
console.log(comments.text());
await client.close();

If you pass an existing runtime ({ runtime }), the factory reuses it; the
returned objectโ€™s close() becomes a no-op.

Flags

| Flag | Description |
| -------------------- | ------------------------------------------------------------------------------------------ | ---------------------------------- |
| --out | Required. .d.ts target for types, .ts target for client. |
| --mode types | client | Output kind (defaults to types). |
| --types-out | Optional override for the .d.ts file when --mode client. Default: derive from --out. |
| --include-optional | Include every parameter (not just the minimum 5 + required). |
| --json | Emit a JSON summary describing the emitted file(s) instead of plain-text logs. |

Testing

tests/emit-ts.test.ts covers:

  • Template rendering (doc comments, Promise<โ€ฆ> return types, proxy wrappers).
  • End-to-end CLI invocation with a stub runtime, ensuring both .ts and .d.ts
    files are written successfully.

2. Official Technical Reference & Guides (openclaw/docs)

File: README.md

openclaw-docs

Mirror repo for the published OpenClaw docs site.

Source of truth lives in openclaw/openclaw, under docs/.

How it works

  1. English docs are authored in openclaw/openclaw.
  2. openclaw/openclaw/.github/workflows/docs-sync-publish.yml mirrors the docs tree into this repo.
  3. This repo stores the published docs tree plus generated locale output.
  4. openclaw/docs/.github/workflows/translate-incremental.yml debounces normal docs changes, while translate-all.yml handles full reconciliation for glossary changes, weekly schedule, release dispatch, or manual dispatch.
  5. .github/workflows/r2-pages.yml builds the full unpruned static site and uploads changed objects to Cloudflare R2.
  6. .github/workflows/pages.yml deploys the small Cloudflare Worker router that preserves clean URLs and markdown negotiation while reading docs from R2.

Translation behavior

  • Locale pages under docs/<locale>/** are generated output.
  • Each translated page stores x-i18n.source_hash.
  • The translate workflow computes a pending file list before calling the model.
  • If no English source hashes changed, the workflow skips the expensive translation step entirely.
  • If files changed, only the pending files are translated.
  • The workflow retries transient model-format failures.
  • Locale outputs are uploaded as artifacts first, then committed together by the finalizer.
  • Incremental and full translation use separate concurrency lanes, so small docs edits do not cancel weekly or glossary-triggered full reconciliation.
  • The weekly scheduled run uses full reconciliation mode to repair missed or flaky locale updates.

Editing rules

  • Do not treat this repo as the primary place for English doc edits.
  • Make English doc changes in openclaw/openclaw, then let sync copy them here.
  • Locale pages under docs/<locale>/** are generated output.
  • .openclaw-sync/source.json records which openclaw/openclaw commit this mirror was synced from.

Static site build

  • npm run docs:build renders the mirrored Mintlify-flavored docs into dist/docs-site.
  • npm run docs:build:cloudflare is the legacy Worker Static Assets fallback build.
  • npm run docs:build:r2 renders the full unpruned site and prepares dist/docs-r2-manifest.json for R2 upload.
  • npm run docs:r2:upload uploads only changed R2 objects, reports cache hits/misses, and refuses to turn a broken remote manifest read into a full-tree reupload.
  • Manual R2 refreshes audit objects before upload; unchanged objects remain cache hits, and transient HEAD failures fall back to the signed manifest. R2_UPLOAD_PUT_ALL=1 is the emergency escape hatch for intentionally rewriting every object.
  • npm run docs:smoke checks representative English and locale pages plus the Pagefind search bundle.
  • npm run docs:check runs both steps.
  • The generated site includes the language picker and static full-text search via Pagefind.
  • Cloudflare deploys workers/docs-router.ts, which serves slashless page URLs, English markdown responses for .md paths or Accept: text/markdown, and /api/search through the DOCS_BUCKET R2 binding.
  • Cloudflare hosting details and limitations are documented in CLOUDFLARE.md.

Secrets

  • OPENCLAW_DOCS_SYNC_TOKEN lives in openclaw/openclaw and lets the source repo push into this repo.
  • OPENCLAW_DOCS_I18N_OPENAI_API_KEY lives in this repo and powers locale translation refreshes.
  • CLOUDFLARE_API_TOKEN lives in this repo and deploys the docs.openclaw.ai router.
  • R2 uploads verify CLOUDFLARE_API_TOKEN, try temporary R2 credentials, and normally fall back to the token-derived direct S3 credential form. OPENCLAW_R2_ACCESS_KEY_ID / OPENCLAW_R2_SECRET_ACCESS_KEY are only fallback upload credentials when the Cloudflare token cannot be verified.

File: docs/index.md


summary: "OpenClaw is a multi-channel gateway for AI agents that runs on any OS."
read_when:

  • Introducing OpenClaw to newcomers
    title: "OpenClaw"

OpenClaw ๐Ÿฆž

"EXFOLIATE! EXFOLIATE!" โ€” A space lobster, probably

Install OpenClaw and bring up the Gateway in minutes. Guided setup with `openclaw onboard` and pairing flows. Link Discord, Signal, Telegram, WhatsApp, and more to chat from anywhere. Launch the browser dashboard for chat, config, and sessions.

Browse docs

Mobile browsers may show the section menu without the full desktop tab bar. Use
these hub links to reach the same top-level docs areas from the page body.

Overview, showcase, first steps, and setup guides. Install paths, updates, containers, hosting, and advanced setup. Messaging channels, pairing, routing, access groups, and channel QA. Architecture, sessions, context, memory, and multi-agent routing. Tools, skills, cron, webhooks, and automation capabilities. Plugin marketplace, publishing, curation, and trust guidance. Providers, model configuration, failover, and local model services. macOS, Windows, iOS, Android, nodes, and web surfaces. Gateway configuration, security, diagnostics, and operations. CLI reference, schemas, RPC, release notes, and templates. Troubleshooting, FAQs, testing, diagnostics, and environment checks.

What is OpenClaw?

OpenClaw is a self-hosted gateway that connects your favorite chat apps โ€” Discord, Google Chat, iMessage, Matrix, Microsoft Teams, Signal, Slack, Telegram, WhatsApp, Zalo, and more via channel plugins โ€” to AI coding agents. You run a single Gateway process on your own machine (or a server), and it becomes the bridge between your messaging apps and an always-available AI assistant.

Who is it for? Developers and power users who want a personal AI assistant they can message from anywhere โ€” without giving up control of their data or relying on a hosted service.

What makes it different?

  • Self-hosted: runs on your hardware, your rules
  • Multi-channel: one Gateway serves every configured channel plugin simultaneously
  • Agent-native: built for coding agents with tool use, sessions, memory, and multi-agent routing
  • Open source: MIT licensed, community-driven

What do you need? Node 26 (recommended), or another supported release: Node 22.22.3+, Node 24.15+, or Node 25.9+. You also need an API key from your chosen provider and 5 minutes. For best quality and security, use the strongest latest-generation model available.

How it works

mermaid
flowchart LR
  A["Chat apps + plugins"] --> B["Gateway"]
  B --> C["OpenClaw agent"]
  B --> D["CLI"]
  B --> E["Web Control UI"]
  B --> F["macOS app"]
  B --> G["iOS and Android nodes"]

The Gateway is the single source of truth for sessions, routing, and channel connections.

Key capabilities

Discord, iMessage, Signal, Slack, Telegram, WhatsApp, WebChat, and more with a single Gateway process. Channel plugins add Matrix, Nostr, Twitch, Zalo, and more; official plugins install on demand. Isolated sessions per agent, workspace, or sender. Send and receive images, audio, and documents. Browser dashboard for chat, config, sessions, and nodes. Pair iOS and Android nodes for camera, screen, and voice-enabled workflows.

Quick start

On npm 12 or npm 11.16+:
text
```bash
npm install openclaw@latest --allow-scripts=openclaw
```

On npm 11.15 and earlier, omit `--allow-scripts=openclaw`.
```bash openclaw onboard --install-daemon ``` Open the Control UI in your browser and send a message:
text
```bash
openclaw dashboard
```

Or connect a channel ([Telegram](/channels/telegram) is fastest) and chat from your phone.

Need the full install and dev setup? See Getting Started.

Dashboard

Open the browser Control UI after the Gateway starts.

Configuration (optional)

Config lives at ~/.openclaw/openclaw.json.

  • If you do nothing, OpenClaw uses the bundled OpenClaw agent runtime; DMs share the agent's main session, and each group chat gets its own session.
  • If you want to lock it down, start with channels.whatsapp.allowFrom and (for groups) mention rules.

Example:

json5
{
  channels: {
    whatsapp: {
      allowFrom: ["+15555550123"],
      groups: { "*": { requireMention: true } },
    },
  },
  messages: { groupChat: { mentionPatterns: ["@openclaw"] } },
}

Start here

All docs and guides, organized by use case. Core Gateway settings, tokens, and provider config. SSH and tailnet access patterns. Channel-specific setup for Discord, Feishu, Microsoft Teams, Telegram, WhatsApp, and more. iOS and Android nodes with pairing, camera, screen, and device actions. Common fixes and troubleshooting entry point.

Learn more

Complete channel, routing, and media capabilities. Workspace isolation and per-agent sessions. Tokens, allowlists, and safety controls. Gateway diagnostics and common errors. Project origins, contributors, and license.