Verify, install, and publish MCP servers, A2A agents, and AI skills.
npx @forge-registry/cli verify @modelcontextprotocol/server-memory
npx @forge-registry/cli install @upstash/context7-mcp
npx @forge-registry/cli policy build --from-config .mcp.json
forge publish
Checks a package before you install it:
- Forge registry — is it listed? verified? who published it?
- OSV scan — known CVEs for the exact version from the OSV database
- Script analysis — lifecycle scripts (
postinstall,preinstall, etc.) checked for suspicious patterns
$ forge verify @modelcontextprotocol/server-memory
@modelcontextprotocol/server-memory · v2026.1.26
──────────────────────────────────────────────────────
PACKAGE
Author Anthropic, PBC
License MIT
REGISTRY
Forge index ✓ Listed
Publisher ✓ @modelcontextprotocol (verified)
forge.json · Not present
SECURITY (scanned 2026-05-27)
Vulnerabilities ✓ None found
Scripts ✓ No suspicious lifecycle scripts
──────────────────────────────────────────────────────
✓ CLEAN & VERIFIED
──────────────────────────────────────────────────────
Options
| Flag | Description |
|---|---|
--json |
Output raw JSON (useful for scripting) |
Exit codes: 0 = clean/verified, 1 = critical vulnerabilities found
Trust-aware wrapper around npm install:
- Runs
forge verifyfirst - Blocks on critical/high vulnerabilities (unless
--force) - Prompts for confirmation if the package is unverified or has warnings
- Delegates to
npm installif you proceed
$ forge install @modelcontextprotocol/server-memory
Options
| Flag | Description |
|---|---|
-f, --force |
Skip verification prompts |
-g, --global |
Install globally (npm install -g) |
--save-dev |
Install as dev dependency |
The check that runs whether or not the model remembers to run it.
forge guard is a Claude Code PreToolUse hook.
It reads the hook payload on stdin, extracts any package coordinate the command
is about to install, and checks it against the registry before the command runs.
forge guard install # register it for this project
forge guard install --global # ~/.claude/settings.json
forge guard install --dry-run # print the settings without writing them
It understands npm install, pnpm/yarn/bun add, npx/uvx/bunx, and the
claude mcp add … -- npx -y <pkg> form, including chained commands.
It blocks one thing and one thing only: a package the registry has revoked. A revocation is an explicit withdrawal, already decided by a human. Everything else — an open CVE, a scan warning — is passed to the model as context to show you, because a trust score is a likelihood judgement from dated evidence and should inform a decision rather than make it.
It fails open. An unreachable registry, an unparseable command, an unlisted package: all of them mean "no opinion", and the command proceeds. A guard that blocks installs when the registry is down is a worse outage than the risk it guards against.
It is silent unless it has something to say — no output on a clean package, and none on a command that installs nothing.
Register a package in the Forge registry. Requires forge login first.
$ forge login # one-time GitHub auth
$ forge publish # run from your package directory
Forge verifies that you own or maintain the source repository before listing the package — a GitHub collaborator check or, for gitlab.com repositories, a GitLab project-membership check. The provider is chosen from your package's repository URL; authenticate with forge login or forge login --gitlab first.
Options
| Flag | Description |
|---|---|
--dir <path> |
Package directory (default: cwd) |
--dry-run |
Validate locally, don't submit |
--sigstore |
Force Sigstore keyless signing (automatic in GitHub Actions) |
Authenticates a source host to verify publisher identity; the token is saved to ~/.forge/config.json.
$ forge login # GitHub — OAuth device flow
$ forge login --gitlab # GitLab — paste a personal access token (read_api scope)
forge publish picks the matching credentials automatically from your package's
repository URL (github.com or gitlab.com). GitLab ownership is confirmed via the
project-membership API (Maintainer/Owner), mirroring the GitHub collaborator check.
| Flag | Description |
|---|---|
--gitlab |
Authenticate with GitLab instead of GitHub |
--token <pat> |
GitLab token, non-interactive (use with --gitlab) |
--logout |
Remove stored credentials (add --gitlab to clear GitLab) |
Search the registry — MCP servers, A2A agents, and skills — by topic, or by the tools an entry actually exposes.
$ forge search "web research"
$ forge search browser --format mcp --verified
Tool-level search answers the question a description search cannot: which
server exposes create_issue? Forge extracts a tool surface from every entry
it scans — statically from the published package, or from a live tools/list
handshake for hosted servers — so --tool matches on what a server exposes
rather than on what its README claims.
$ forge search --tool create_issue
$ forge search --tool write --privileged --sort trust # who can write, most trusted first
$ forge search files --privileged=false # observed surface with nothing privileged
Each hit says which tool matched (↳ exposes write_file ⚑, where ⚑ marks
a privileged tool — shell, file write, delete-class) and when that surface was
observed.
A tool surface is a point-in-time scan artifact, so a tool query necessarily
searches the entries Forge has been able to read. The footer reports how many
that is, and every result carries either an observed surface or a stated reason
there is none (not-scanned, not-extractable, none-detected,
not-applicable). No result never means "no such server exists", and a
missing surface never means "this server exposes no tools".
Impact search asks a third question, and a genuinely different one: how far would a compromise of this reach? Blast radius is scored from the tool surface, the credential kind, where the code runs, and how much one install pulls in — and it is never part of the trust score. A high blast radius is not a defect; a filesystem server is supposed to write files.
$ forge search --blast critical # the far-reaching ones
$ forge search "database" --blast contained
$ forge search --blast unknown --format mcp # what Forge could not measure
--blast unknown selects exactly the entries whose radius Forge could not pin
to a band. Every other value excludes them, because "we never looked" is not
a low blast radius — and a filter that quietly returned the least-known entries
under the most reassuring name would be the worst default available. Results
show ↳ blast radius ~ moderate–critical (not fully measured) for those, and
forge verify <id> prints the full breakdown with the gaps named. The model is
documented at https://forgeregistry.com/blast-radius.
Quality search works the same way, with the same caveat attached for the same reason:
$ forge search --min-accuracy 0.9 # independently reproduced ≥ 90%
$ forge search sql --min-accuracy 0.8 --format mcp
Only independently reproduced eval results satisfy --min-accuracy. A
publisher's own number never does — a filter that honoured self-reported
accuracy would rank the people willing to type one above everyone who wasn't.
A contested result doesn't qualify either: its accuracy is what's in dispute.
Almost nothing in the registry has been evaluated yet, so an empty result here
means unmeasured, not nothing is that good; the footer says how little has
been measured. See forge eval.
| Flag | Effect |
|---|---|
--tool <name> |
only entries observed to expose a matching tool |
--privileged / --privileged=false |
observed surface does / does not include a privileged tool |
--blast <band> |
contained, moderate, extensive, critical, or unknown (the unmeasured) |
--min-accuracy <0-1> |
only entries with an independently reproduced eval accuracy at or above this |
--sort <key> |
relevance (default), downloads, stars, recent, trust |
--format, --verified, --min-stars, --limit, --json |
as elsewhere |
Report a package's MCP tool surface before you install it — statically, by extracting declared tool names from the npm tarball. The package is never executed. Privileged tools (shell / exec / write / delete-class) are flagged.
$ forge inspect @modelcontextprotocol/server-filesystem
$ forge inspect @some/server --run # ALSO spawn the server for live tools/list
--run installs with --ignore-scripts into a temp dir, spawns the server with
a scrubbed environment and a hard timeout, and queries tools/list — then diffs
runtime reality against the static declaration. It executes package code (process
isolation only, not a container).
Run a package's install scripts inside a network-isolated Docker container
and watch what they do — the install-time attack surface static analysis can't
see. Two-phase: dependencies are fetched first (--ignore-scripts), then the
lifecycle scripts run under --network=none, so any network attempt or stray
filesystem write is the package's own. Requires Docker; skips cleanly without it.
$ forge sandbox @upstash/context7-mcp
Produce a signed policy artifact for a set of registry entries, and verify one offline. Built for CI gates and for MCP gateway operators who need Forge's trust data in a form they can enforce without trusting the network it arrived over.
$ forge policy build --from-config .mcp.json --min-score 70 --require-identity
$ forge policy verify forge.policy.json
build resolves the servers (from arguments, an MCP client config's
mcpServers, or a forge.lock.json), fetches the artifact, verifies it before
writing it to disk, and exits non-zero if anything is denied. verify checks a
saved artifact with no network at all: signature against the pinned registry key,
statement type, expiry, and a recomputation of every decision from the artifact's
own recorded evidence.
Thresholds: --min-score, --max-scan-age, --max-severity,
--require-identity, --require-provenance, --deny-privileged. Whatever you
pass is recorded inside the artifact, so a reader downstream always knows what
was applied.
Exit codes are deliberately distinct — 1 means the artifact is trustworthy and
an entry failed policy (a finding); 2 means the artifact itself could not be
trusted (an incident).
Format reference: spec/POLICY.md. Gateway integration: docs/GATEWAY-INTEGRATION.md.
Ask the registry what changed in the packages this project already depends
on — the question forge audit cannot answer, because audit reports the current
state and this reports the diff between two security scans.
$ forge changes
$ forge changes --since 7 --severity critical
$ forge changes --breaking # only what will break your code
$ forge changes --all --json # include user-level MCP configs
Reads dependency names from .mcp.json, .cursor/mcp.json, .vscode/mcp.json
and package.json, then asks the registry which of them have a scan diff on
record: a new npm maintainer, a new install script, obfuscation that was not
there before, a tool surface that grew a privileged capability, a
newly-applicable CVE, lost build provenance, or a revoked publisher.
It also reports the two changes that are about compatibility rather than
risk — a tool that was removed, and an input schema that got stricter (a
parameter dropped, made required, or retyped). Those arrive as warnings, because
nobody was compromised, which is also exactly how the one line that will break
tomorrow's build gets lost among the others: they are tagged BREAKING instead
of by severity, counted separately, and --breaking narrows the report to them.
No login required. Exits 1 when anything critical is reported — or, under
--breaking, when any compatibility break is — so it works as a CI gate in
front of a dependency bump. A registry it could not reach exits 0 with a warning
and says so explicitly: "we could not ask" is never printed as "nothing changed".
Silence means unchanged, not unscanned: names with nothing on record come
back in quiet. Run it on a schedule — a nightly CI job is the intended
shape — rather than waiting to be told: push subscriptions are not currently
offered.
Resolve every MCP server and package this project declares to an exact
version, and write the result to forge.lock.json.
$ forge lock # resolve, keeping any pins already in the file
$ forge lock --update # move every pin to the newest version its range permits
$ forge lock --audit # + check every locked package for known CVEs
$ forge lock --dry-run # report without writing
npm already locks npm. What nothing locked until now is the set of MCP servers a
project launches, because those live in .mcp.json / .cursor/mcp.json as
npx -y @scope/pkg — an instruction to fetch and run whatever npm serves as
latest, on every run, on every machine. forge lock reads the same config
files forge audit and forge changes read (plus package.json
dependencies), walks the graph, and writes down what it found.
| Option | |
|---|---|
--audit |
check every locked package against OSV |
--update |
re-resolve every range to the newest version it still permits |
--all |
also read user-level MCP configs (~/.claude, Claude Desktop) |
-o, --output <file> |
write somewhere other than forge.lock.json |
--dry-run |
resolve and report, but do not write |
--json |
output raw JSON instead of formatted text |
A run with no flags does not re-pin. It re-crawls — the edges below a pin
only exist in npm's metadata — but selects the version already in the lock file
wherever that version still satisfies the declared range. A pin is dropped only
when the range no longer admits it. --update is the one flag that moves a pin
forward.
The file is byte-deterministic. Sorted keys, sorted arrays, and no timestamp or generator stamp anywhere in it, so the same inputs write the same bytes. The diff is the point: it is where you see that a server you never touched moved underneath you. Commit it.
Compatibility checking. When two dependents demand ranges that no published version satisfies at once, the lock cannot honour both, and that pair is reported with both ranges and both requiring paths — not a count:
COMPATIBILITY (1)
! WARNING zod locked to 3.9.0
^2.0.0 legacy-server
^3.0.0 @upstash/context7-mcp
no published version of zod satisfies both ^2.0.0 (required by
legacy-server@1.0.0) and ^3.0.0 (required by …); locked to 3.9.0,
which satisfies ^3.0.0
Conflicts are a warning and never fail a build: two manifests disagreeing is a fact about somebody else's dependencies, not a security event. Only ranges the resolver fully understands are compared — a pair it merely failed to parse is not evidence of a conflict.
Read the gaps. The crawl is capped at 60 packages and depth 4, the same
limits as the registry's own scanner. Every dependency that was declared and
never landed is listed in unresolved[] with a reason, truncated says whether
a cap bit, and a node whose edge list was cut short carries
dependenciesKnown: false. An empty dependencies with dependenciesKnown: true means the package depends on nothing; with false it means nobody
finished looking.
Exit codes: 0 resolved (including with conflicts) · 1 --audit found a
critical or high advisory · 0 --audit could not reach the advisory source,
with a loud warning — same rule as forge changes, because a gate that goes red
on someone else's outage is a gate people delete.
Format reference: spec/LOCK.md · schema: spec/forge-lock.schema.json. Feeds straight into the policy gate:
forge lock --audit && forge policy build --from-lock forge.lock.json --fail-on warnExports the entry's CycloneDX 1.6 bill of materials, with VEX statements in the same document. Built for CI.
forge sbom @upstash/context7-mcp > sbom.cdx.json
forge sbom @upstash/context7-mcp -o sbom.cdx.json --summary| Option | |
|---|---|
-o, --output <file> |
write the document to a file, plus a <file>.sig sidecar |
--summary |
print a human-readable completeness summary instead of the document |
--no-sig |
skip the sidecar |
--require-inventory |
exit 2 when the document carries no dependency inventory |
Without -o or --summary the document goes to stdout and everything else to
stderr, so redirects stay clean.
Read the completeness fields. Forge's dependency resolver is capped (60
packages, depth 4) and never claims a complete SBOM. Every document states, in
compositions[].aggregate and in forge:sbom:* properties, which limits
applied and which declared dependencies were never resolved. VEX statements are
always in_triage: Forge establishes that a vulnerable version is present,
never that it is exploitable. Full contract:
docs/SBOM.md.
The response is Ed25519-signed and verified before anything is written. Because
HTTP headers do not survive a download, -o writes the signature beside the
document so it stays verifiable later.
Exit codes: 0 verified · 1 not found / unreachable / bad signature · 2
--require-inventory unmet.
Run an eval suite against a local package directory, a published npm package, a local skill folder, or a published skill — and optionally report the result to the registry.
$ forge eval evals/forge-mcp-smoke-v1.json . # what you're building
$ forge eval evals/forge-mcp-smoke-v1.json @scope/pkg # someone else's, from npm
$ forge eval suite.json @scope/pkg --publish # report it, attributably
$ forge eval suite.json . --env BRAVE_API_KEY # pass one variable through
$ forge eval evals/forge-skill-quality-v1.json code-reviewer # a published skill
$ forge eval evals/forge-skill-quality-v1.json ./my-skill # a skill on disk
EVAL (FORGE/MCP-SMOKE-V1@1.0.0)
Subject @acme/server v0.4.2
Suite digest sha256:a1e906f1c653c9c0…
✓ handshake-and-advertise-a-tool 14 ms
✓ tool-names-carry-no-whitespace 0 ms
✗ rejects-an-undeclared-tool 31 ms
expected an error, got a result
Accuracy 66.7% (2/3)
Latency p50 14 ms · p99 31 ms (this machine)
Transcript sha256:3dd9436fa2bc670f…
Reporting someone else's package is the point. Forge labels a result
reproduced only when two independent accounts ran the same pinned suite
against the same version and agreed. A publisher's own run is filed as an
unverified claim and displayed as one, so the second run — yours, on a package
you didn't write — is what turns a number into evidence. --publish needs
forge login; the registry resolves your token to a GitHub account and decides
whether you are the listing's publisher or a third party. You cannot report
that for yourself.
Suites are JSON files conforming to
spec/eval-suite.schema.json. Assertions are
deterministic only — substrings, a regular expression, error-or-not — because a
model-judged number stops being reproducible as soon as the judge model or its
sampling settings change.
Two kinds of suite. mcp-tools calls a server's tools and asserts on the
responses; it executes the package (see the warning below). skill-static
names a field of a skill — name, description, body, license,
source_url, keywords — and asserts over that text, executing nothing. Same
predicates for both: an absent or blank field reads as an error, so
{"error": true} means "this field is absent". kind is inside the suite
digest, so the same cases under a different kind can never corroborate each
other. Cross-kind fields are rejected rather than ignored — a tool on a
static case fails validation instead of quietly becoming a text assertion.
A static suite reports no latency: there is no round trip to time, and the
output omits the row rather than printing a confident p50 0 ms.
An incomplete run is never reported: the registry rejects a run whose case count differs from the suite's, so running only the cases you pass is not a route to a good number.
mcp-tools suite executes the package — installed with
--ignore-scripts into a temp directory and spawned with a scrubbed
environment. That is process isolation, not a container; forge sandbox is the
Docker one. Environment variables reach the server only when you name them with
--env. A skill-static suite executes nothing, and the warning is suppressed
for it — a banner that is false half the time is a banner nobody reads.
Find reproduced results: forge search --min-accuracy 0.9.
Runs the Forge CLI as an MCP server over stdio, so AI clients (Claude Code, Cursor, any MCP-compatible client) can search, vet, and install Forge packages inline — no terminal round-trip.
Add to your client's MCP config (.mcp.json, .cursor/mcp.json, .vscode/mcp.json, Antigravity's ~/.gemini/config/mcp_config.json, …):
{
"mcpServers": {
"forge": {
"command": "npx",
"args": ["-y", "@forge-registry/cli", "mcp"]
}
}
}Tools exposed — "hosted" marks the ones the remote endpoint also serves (see below).
| Tool | Description | Hosted |
|---|---|---|
forge_search |
Search the registry (MCP servers, A2A agents, skills) by topic or by exposed tool (tool, privileged) or by impact (blast), with verification filters and sort: trust; hits report matched_tools, their tool-surface freshness, and a blast_radius |
✓ |
forge_get_package |
Full trust profile: verification, revocation, live OSV CVE scan, install-script analysis | ✓ |
forge_alternatives |
Functional substitutes for an entry — other servers observed to expose the same tools, ranked better-trusted first, each reporting what the swap does not cover and what capability or credentials it adds | ✓ |
forge_get_package |
Full trust profile: verification, revocation, live OSV CVE scan, install-script analysis, and any eval results (labelled reproduced / contested / unverified claim) | ✓ |
forge_audit |
Audit every MCP server configured in a project (.mcp.json, .cursor/mcp.json, .vscode/mcp.json) |
— |
forge_get_skill |
Fetch a skill's full prompt and metadata | ✓ |
forge_add_skill |
Install a skill into the project (writes .claude/skills/<id>/SKILL.md, .cursorrules, or Copilot instructions) |
— |
forge_inspect |
Statically extract a package's declared MCP tool surface (never executes it); flags privileged tools | ✓ |
forge_install_plan |
Everything needed to install one entry in one call: per-client config (Claude Code, Cursor, VS Code, Gemini CLI), credential placeholders, trust verdict, and blocking warnings — configs are withheld when a finding blocks | ✓ |
forge_submit_package |
Point the registry at an unindexed package (npm / PyPI / GitHub / remote URL) — validated server-side, no auth needed | ✓ |
forge_changes |
What changed in a list of packages since a given date — scan diffs, not current state | ✓ |
forge_publish |
Register or claim a package in the Forge registry (requires forge login in the terminal first) |
— |
The same tools are served over HTTP at https://forgeregistry.com/api/mcp (MCP
Streamable HTTP transport, no authentication). Clients that support remote MCP servers
can connect with the URL alone:
{
"mcpServers": {
"forge": {
"type": "http",
"url": "https://forgeregistry.com/api/mcp"
}
}
}Or, in Claude Code: claude mcp add --transport http forge https://forgeregistry.com/api/mcp.
The three tools marked "—" above are stdio-only, and not because of an omission: they act
on your machine — reading your MCP configs, writing a skill into your project, signing a
publish with the keys in ~/.forge — which a server running in the registry cannot do.
Run forge mcp locally when you want those.
Credentials and settings are stored in ~/.forge/config.json:
{
"githubToken": "gho_...",
"githubLogin": "your-github-handle",
"registryUrl": "https://forgeregistry.com"
}Environment variables (override config file):
| Variable | Description |
|---|---|
FORGE_GITHUB_TOKEN |
GitHub access token |
FORGE_REGISTRY_URL |
Registry URL (default: https://forgeregistry.com) |
FORGE_REQUIRE_SIGNED |
Set to 1 to reject unsigned registry responses (recommended in CI) |
FORGE_REGISTRY_PUBKEY |
Override the pinned response-signing public key (self-hosted registries). Also used to verify policy artifacts. |
cd cli
npm install
npm run build # compile TypeScript → dist/
npm run dev -- verify @some/package # run without building
To test against a local registry:
FORGE_REGISTRY_URL=http://localhost:3000 forge verify @some/package
MIT — forgeregistry.com