Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,26 @@ The tool supports these operations through one input schema:
// { operation: 'bulk-packages', purls: ['pkg:npm/lodash', 'pkg:cargo/serde'], concurrency?: number }
```

### MCP

The package ships an MCP server with six read-only tools for metadata, versions, dependencies, maintainers, bulk lookups, and ecosystem discovery:

```bash
claude mcp add registries --scope user -- npx @agntn/registries mcp
```

Hosts with their own transport can import `createMcpServer` from `@agntn/registries/mcp`.

### Pi and OMP extensions

Install the published package in OMP:

```bash
omp install @agntn/registries
```

From a source checkout, use `omp install .`. The package manifests point OMP and Pi at their matching extension entrypoints, and both expose the same six read-only tools as MCP.

## Registries

| Ecosystem | PURL type | Registry |
Expand Down Expand Up @@ -231,6 +251,7 @@ registries <command> [options]
| `registries versions <purl>` | List all published versions |
| `registries deps <purl>` | Dependencies for a specific version |
| `registries maintainers <purl>` | Package maintainers / authors |
| `registries mcp` | Start the MCP server over stdio |
| `registries cache status` | Show cache stats (entries, freshness) |
| `registries cache path` | Print cache directory path |
| `registries cache clear` | Remove all cached data |
Expand Down
43 changes: 25 additions & 18 deletions build.config.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,24 @@
import { defineBuildConfig } from "obuild/config";
import type { BuildConfig } from "obuild";

const bundleTypeboxHook: NonNullable<BuildConfig["hooks"]> = {
rolldownConfig(config) {
const originalExternal = config.external;
const isTypebox = (id: string) => /^typebox(?:\/|$)/.test(id);

if (typeof originalExternal === "function") {
config.external = async (id, importer, isResolved) =>
isTypebox(id) ? false : originalExternal(id, importer, isResolved);
return;
}

if (Array.isArray(originalExternal)) {
config.external = originalExternal.filter(
(item) => !(typeof item === "string" && isTypebox(item)),
);
}
},
};

export default defineBuildConfig({
entries: [
Expand All @@ -7,27 +27,14 @@ export default defineBuildConfig({
input: [
"./src/index.ts",
"./src/ai.ts",
"./src/types.ts",
"./src/core/index.ts",
"./src/registries/index.ts",
"./src/cli.ts",
"./src/commands/shared.ts",
"./src/commands/info.ts",
"./src/commands/versions.ts",
"./src/commands/deps.ts",
"./src/commands/maintainers.ts",
"./src/commands/cache.ts",

"./src/cache/index.ts",

"./src/cache/paths.ts",

"./src/cache/lockfile.ts",

"./src/cache/storage.ts",

"./src/cache/cached-registry.ts",
"./src/cli.ts",
"./src/mcp.ts",
"./src/tool-operations.ts",
],
dts: true,
},
],
hooks: bundleTypeboxHook,
});
2 changes: 1 addition & 1 deletion oxlint.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export default defineConfig({
{ from: "file", name: "ToolResult" },
{
from: "package",
name: "ExtensionAPI",
name: ["ExtensionAPI", "ToolDefinition"],
package: "@earendil-works/pi-coding-agent",
},
{
Expand Down
51 changes: 46 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@
"description": "Universal package registry client. Query npm, PyPI, crates.io, RubyGems, Packagist and more with a single PURL-native API.",
"keywords": [
"crates",
"mcp",
"metadata",
"npm",
"package-registry",
"packagist",
"pi-extension",
"purl",
"pypi",
"rubygems",
Expand All @@ -25,7 +27,9 @@
"registries": "./dist/cli.mjs"
},
"files": [
"dist"
"dist",
"packages/pi/extensions",
"packages/omp/extensions"
],
"type": "module",
"sideEffects": [
Expand All @@ -46,6 +50,10 @@
"types": "./dist/ai.d.mts",
"import": "./dist/ai.mjs"
},
"./mcp": {
"types": "./dist/mcp.d.mts",
"import": "./dist/mcp.mjs"
},
"./registries": {
"types": "./dist/registries/index.d.mts",
"import": "./dist/registries/index.mjs"
Expand All @@ -61,9 +69,10 @@
"scripts": {
"build": "obuild",
"dev:prepare": "obuild --stub",
"fmt": "oxlint . --fix && oxfmt .",
"lint": "oxlint . && oxfmt --check .",
"typecheck": "tsc --noEmit && tsc -p test/tsconfig.json --noEmit",
"fmt": "pnpm lint:fix",
"lint": "pnpm build && oxlint . && oxfmt --check .",
"lint:fix": "oxlint . --fix && oxfmt .",
"typecheck": "tsc --noEmit && tsc -p test/tsconfig.json --noEmit && pnpm build && tsc -p tsconfig.extensions.json --noEmit",
"test": "vitest",
"test:run": "vitest run",
"prepack": "pnpm run build",
Expand All @@ -72,13 +81,18 @@
"docs:build": "pnpm --dir docs build"
},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.30.0",
"citty": "^0.2.1",
"consola": "^3.4.2",
"ofetch": "^1.5.1",
"typebox": "^1.3.25",
"unstorage": "^1.17.4"
},
"devDependencies": {
"@agntn/ox": "^0.1.0",
"@earendil-works/pi-coding-agent": "0.84.2",
"@oh-my-pi/omptype": "18.0.0",
"@oh-my-pi/pi-coding-agent": "18.0.0",
"@types/node": "^25.3.0",
"ai": "^6.0.116",
"changelogen": "^0.6.2",
Expand All @@ -91,10 +105,18 @@
"zod": "^4.3.6"
},
"peerDependencies": {
"@earendil-works/pi-coding-agent": ">=0.84.2",
"@oh-my-pi/pi-coding-agent": ">=18.0.0",
"ai": "^6.0.116",
"zod": "^4.3.6"
},
"peerDependenciesMeta": {
"@earendil-works/pi-coding-agent": {
"optional": true
},
"@oh-my-pi/pi-coding-agent": {
"optional": true
},
"ai": {
"optional": true
},
Expand All @@ -105,5 +127,24 @@
"engines": {
"node": ">=22.6.0"
},
"packageManager": "pnpm@11.3.0"
"packageManager": "pnpm@11.3.0",
"mcpServers": {
"registries": {
"command": "node",
"args": [
"dist/cli.mjs",
"mcp"
]
}
},
"omp": {
"extensions": [
"./packages/omp/extensions/registries.ts"
]
},
"pi": {
"extensions": [
"./packages/pi/extensions/registries.ts"
]
}
}
10 changes: 10 additions & 0 deletions packages/AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Agent runtime integrations

This subtree contains distributable Pi and OMP extensions for `@agntn/registries`.

- Extension entrypoints must stay source-first for local development and fall back to built files from `dist/` in packed installations.
- Pi may choose the source or distribution module dynamically.
- OMP requires literal imports for both source and distribution paths because its loader rewrites visible imports.
- Tool execution belongs in `src/tool-operations.ts`; extensions only declare host metadata and delegate.
- All registry operations are read-only. OMP tools declare `approval: "read"`.
- Keep tool names, schemas, descriptions, and tests aligned across Pi, OMP, and MCP.
112 changes: 112 additions & 0 deletions packages/omp/extensions/registries.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import { existsSync } from "node:fs";
import { fileURLToPath } from "node:url";

import type { AgentToolResult, ExtensionAPI } from "@oh-my-pi/pi-coding-agent";

import type * as RegistryTools from "../../../dist/tool-operations.d.mts";

const sourceModulePath = fileURLToPath(new URL("../../../src/tool-operations.ts", import.meta.url));
let toolOperationsPromise: Promise<typeof RegistryTools> | undefined;

function loadToolOperations(): Promise<typeof RegistryTools> {
toolOperationsPromise ??= (
existsSync(sourceModulePath)
? import("../../../src/tool-operations.ts")
: import("../../../dist/tool-operations.mjs")
) as Promise<typeof RegistryTools>;
return toolOperationsPromise;
}

type RegistryToolResult = AgentToolResult<unknown>;

export default function registriesExtension(pi: ExtensionAPI): void {
const { Type } = pi.typebox;
const PURL = Type.String({
minLength: 1,
maxLength: 2_048,
description: "Package URL, for example pkg:npm/lodash or pkg:pypi/requests@2.32.3",
});
const PURLParams = Type.Object({ purl: PURL }, { additionalProperties: false });
const BulkPackagesParams = Type.Object(
{
purls: Type.Array(PURL, { minItems: 1, maxItems: 50 }),
concurrency: Type.Optional(Type.Integer({ minimum: 1, maximum: 50 })),
},
{ additionalProperties: false },
);
const EmptyParams = Type.Object({}, { additionalProperties: false });

pi.setLabel("Registries");

pi.registerTool({
name: "registries_package",
label: "Registry Package",
description: "Fetch normalized metadata for one package URL",
parameters: PURLParams,
approval: "read",
async execute(_toolCallId, params, signal): Promise<RegistryToolResult> {
const { packageOperation } = await loadToolOperations();
return packageOperation(params, signal);
},
});

pi.registerTool({
name: "registries_versions",
label: "Registry Versions",
description: "List normalized versions for one package URL",
parameters: PURLParams,
approval: "read",
async execute(_toolCallId, params, signal): Promise<RegistryToolResult> {
const { versionsOperation } = await loadToolOperations();
return versionsOperation(params, signal);
},
});

pi.registerTool({
name: "registries_dependencies",
label: "Registry Dependencies",
description: "List dependencies for a versioned package URL",
parameters: PURLParams,
approval: "read",
async execute(_toolCallId, params, signal): Promise<RegistryToolResult> {
const { dependenciesOperation } = await loadToolOperations();
return dependenciesOperation(params, signal);
},
});

pi.registerTool({
name: "registries_maintainers",
label: "Registry Maintainers",
description: "List normalized maintainers for one package URL",
parameters: PURLParams,
approval: "read",
async execute(_toolCallId, params, signal): Promise<RegistryToolResult> {
const { maintainersOperation } = await loadToolOperations();
return maintainersOperation(params, signal);
},
});

pi.registerTool({
name: "registries_bulk_packages",
label: "Registry Bulk Packages",
description: "Fetch normalized metadata for up to 50 package URLs",
parameters: BulkPackagesParams,
approval: "read",
async execute(_toolCallId, params, signal): Promise<RegistryToolResult> {
const { bulkPackagesOperation } = await loadToolOperations();
return bulkPackagesOperation(params, signal);
},
});

pi.registerTool({
name: "registries_ecosystems",
label: "Registry Ecosystems",
description: "List package ecosystems registered by this package",
parameters: EmptyParams,
approval: "read",
async execute(): Promise<RegistryToolResult> {
const { ecosystemsOperation } = await loadToolOperations();
return ecosystemsOperation();
},
});
}
Loading
Loading