-
Notifications
You must be signed in to change notification settings - Fork 120
feat(#4795): implement MCP registry annotation projection #4859
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
23 commits
Select commit
Hold shift + click to select a range
4c786c8
feat(#4795): implement MCP registry annotation projection
fullsend-ai-coder[bot] b26c23b
fix(#4795): address review feedback on annotation projection
fullsend-ai-coder[bot] 511f7c9
fix(#4795): address review feedback on annotation projection
fullsend-ai-coder[bot] 4511619
refactor(#4795): split annotation projection scalar walker
michael-valdron 03647c4
refactor(#4795): split projectAnnotations resolution pipeline
michael-valdron b2c8575
test(#4795): use toHaveLength for collision key count assertion
michael-valdron 4e98c17
fix(#4795): address review feedback on annotation projection
michael-valdron 0636e0a
fix(#4795): address review feedback on annotation projection
fullsend-ai-coder[bot] 5593869
fix(#4795): share URL parse helper and document D9 schema refs
michael-valdron 821d6c1
fix(#4795): address SonarCloud findings in util helpers
michael-valdron 3cdcf74
test(#4795): add rewritten MCP registry server.json example fixtures
michael-valdron ed34c0b
fix(#4795): validate server.json via $schema on mapping entrypoints
michael-valdron b8a4988
feat(#4795): allow D8 placeholder URL override before websiteUrl
michael-valdron e215cf0
fix(#4795): remove 'Step XX:' prefixes from annotationProjection.ts
michael-valdron 10e0d21
feat(#4795): structurally validate closed McpServerDocument shapes
michael-valdron 16f626c
docs(#4795): expand annotation projection changeset with follow-on fe…
michael-valdron 0f745ba
revert(#4795): restore mcp-registry-server-mapping openspec to pre-br…
michael-valdron c475f12
refactor(#4795): group annotation projection module constants at file…
michael-valdron 3e5c9fc
fix(#4795): address SonarCloud findings in server.json util validators
michael-valdron fbda723
fix(#4795): address review feedback on PR #4859
fullsend-ai-coder[bot] d34a1b9
docs(#4795): add mcp-registry-server-mapping-common plugin README
michael-valdron fde889d
docs(#4795): pin server.json schema refs to MCP Registry v1.8.1
michael-valdron ce3fd2d
docs(#4795): document server.json TypeScript shapes
michael-valdron File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
5 changes: 5 additions & 0 deletions
5
workspaces/ai-integrations/.changeset/mcp-registry-annotation-projection.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| --- | ||
| '@red-hat-developer-hub/backstage-plugin-mcp-registry-server-mapping-common': minor | ||
| --- | ||
|
|
||
| Add MCP Registry annotation projection: recursive scalar-leaf walker that projects unmapped server.json attributes into `modelcontextprotocol.io/*` annotations with D3 key sanitization, D9 secret redaction, D11 URL gating, and D12 null/empty omission. Also add closed schema-aligned `McpServerDocument` typing, structural `assertServerJsonSchema` validation on mapping/projection entrypoints (including required `$schema` with `server.schema.json` basename), and an optional D8 `placeholderRemoteUrl` override (D11-checked before `websiteUrl` fallback). | ||
123 changes: 123 additions & 0 deletions
123
workspaces/ai-integrations/plugins/mcp-registry-server-mapping-common/README.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,123 @@ | ||
| # @red-hat-developer-hub/backstage-plugin-mcp-registry-server-mapping-common | ||
|
|
||
| Deterministic transform from [MCP Registry](https://github.com/modelcontextprotocol/registry) | ||
| **v1.8.1** | ||
| [`server.json`](https://raw.githubusercontent.com/modelcontextprotocol/registry/v1.8.1/docs/reference/server-json/draft/server.schema.json) | ||
| documents to Backstage `API` entities with `spec.type: mcp-server`. | ||
|
|
||
| This common library is a pure mapping contract (no I/O, no registry client). | ||
| It is intended for consumers such as a future `mcp-registry-provider` catalog | ||
| entity provider. | ||
|
|
||
| ## Install | ||
|
|
||
| ```bash | ||
| yarn add @red-hat-developer-hub/backstage-plugin-mcp-registry-server-mapping-common | ||
| ``` | ||
|
|
||
| ## Usage | ||
|
|
||
| ### Map MCP `server.json` → Backstage `McpServerApiEntity` entity | ||
|
|
||
| ```ts | ||
| import { | ||
| mapServerToEntity, | ||
| projectAnnotations, | ||
| } from '@red-hat-developer-hub/backstage-plugin-mcp-registry-server-mapping-common'; | ||
| import type { McpServerDocument } from '@red-hat-developer-hub/backstage-plugin-mcp-registry-server-mapping-common'; | ||
|
|
||
| const doc: McpServerDocument = { | ||
| $schema: | ||
| 'https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json', | ||
| name: 'io.github.example/weather', | ||
| description: 'Weather MCP server', | ||
| version: '1.0.0', | ||
| remotes: [{ type: 'streamable-http', url: 'https://example.com/mcp' }], | ||
| }; | ||
|
|
||
| const { entity, consumedPaths, reservedAnnotationKeys } = mapServerToEntity( | ||
| doc, | ||
| { | ||
| owner: 'group:default/platform', | ||
| lifecycle: 'production', | ||
| // Optional D8 placeholder when no valid remotes remain (tried before websiteUrl) | ||
| // placeholderRemoteUrl: 'https://example.com/placeholder', | ||
| }, | ||
| ); | ||
|
|
||
| const projected = projectAnnotations( | ||
| doc, | ||
| consumedPaths, | ||
| reservedAnnotationKeys, | ||
| ); | ||
|
|
||
| entity.metadata.annotations = { | ||
| ...entity.metadata.annotations, | ||
| ...projected, | ||
| }; | ||
| ``` | ||
|
|
||
| `mapServerToEntity` performs direct field mapping (identity, remotes, links, | ||
| dedicated annotations) and returns hand-off data for projection. | ||
| `projectAnnotations` emits remaining scalars as `modelcontextprotocol.io/*` | ||
| annotations without overwriting reserved keys. | ||
|
|
||
| ### Input requirements | ||
|
|
||
| Documents must be structurally valid `McpServerDocument` values: | ||
|
|
||
| - `$schema` is required and must be an absolute URL whose basename is | ||
| `server.schema.json` | ||
| - `name`, `description`, and `version` are required | ||
| - Only ServerDetail fields from the draft schema are allowed (closed shape) | ||
| - Nested transports, packages, icons, and inputs are type-checked at runtime | ||
|
|
||
| Invalid input throws `TypeError` (or `Error` for missing required mapping | ||
| fields) with an actionable message. | ||
|
|
||
| ## `server.json` types | ||
|
|
||
| Field-level breakdowns of the MCP Registry **v1.8.1** `server.json` TypeScript | ||
| shapes live in [`docs/server-json-types.md`](./docs/server-json-types.md) | ||
| (source of truth: [`src/types.ts`](./src/types.ts)). | ||
|
|
||
| ### Caller defaults (`McpServerMappingDefaults`) | ||
|
|
||
| | Field | Default | Purpose | | ||
| | ---------------------- | -------------- | ----------------------------------------------- | | ||
| | `prefix` | `mcp.registry` | Prefix for derived `metadata.name` | | ||
| | `owner` | `unknown` | `spec.owner` | | ||
| | `lifecycle` | `production` | `spec.lifecycle` | | ||
| | `placeholderRemoteUrl` | _(none)_ | D8 placeholder URL before `websiteUrl` fallback | | ||
|
|
||
| ## Mapping behavior (summary) | ||
|
|
||
| - **Identity** — `metadata.name` from sanitized `name` + `version` (+ optional prefix); `modelcontextprotocol.io/name` and `modelcontextprotocol.io/version` reserved | ||
| - **Remotes (D8/D11)** — copy `http`/`https` remotes with non-empty `type`; otherwise synthesize `{ type: "undefined", url }` from `placeholderRemoteUrl` then `websiteUrl` | ||
| - **URLs (D11)** — only absolute `http`/`https` are emitted on links, remotes, and projected URL annotations | ||
| - **Repository (D10)** — SCM-aware Source Code link + `backstage.io/source-location`; original URL on `modelcontextprotocol.io/repository.url` | ||
| - **Projection** — unmapped scalars under `modelcontextprotocol.io/<path>` with key sanitization, collision hashing, D9 secret redaction, and D12 null/empty omission | ||
|
|
||
| Design decisions and scenarios live under | ||
| [`openspec/changes/mcp-registry-server-mapping/`](../../openspec/changes/mcp-registry-server-mapping/). | ||
|
|
||
| ## Examples | ||
|
|
||
| See [`examples/server-json/`](./examples/server-json/) for rewritten MCP Registry | ||
| `server.json` fixtures useful for local testing. | ||
|
|
||
| ## Development | ||
|
|
||
| From the `workspaces/ai-integrations` workspace root: | ||
|
|
||
| ```bash | ||
| # Type-check | ||
| yarn tsc | ||
|
|
||
| # Unit tests for this package | ||
| yarn test -- plugins/mcp-registry-server-mapping-common/src | ||
|
|
||
| # Lint / API report (when public exports change) | ||
| yarn lint:all | ||
| yarn build:api-reports:only | ||
| ``` |
179 changes: 179 additions & 0 deletions
179
...tegrations/plugins/mcp-registry-server-mapping-common/docs/server-json-types.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,179 @@ | ||
| # `server.json` types | ||
|
|
||
| TypeScript shapes for MCP Registry **v1.8.1** | ||
| [`server.schema.json`](https://raw.githubusercontent.com/modelcontextprotocol/registry/v1.8.1/docs/reference/server-json/draft/server.schema.json) | ||
| live in [`src/types.ts`](../src/types.ts). Each table below mirrors one exported | ||
| type; headings link to the declaration in source. | ||
|
|
||
| ## [`McpServerDocument`](../src/types.ts#L279) | ||
|
|
||
| Root `server.json` document (`ServerDetail`). Closed shape — only these fields | ||
| are allowed. | ||
|
|
||
| | Field | Type | Required | Description | | ||
| | ------------- | ---------------------------------------------- | -------- | --------------------------------------------------------------- | | ||
| | `$schema` | `string` | yes | Absolute JSON Schema URI whose basename is `server.schema.json` | | ||
| | `name` | `string` | yes | Reverse-DNS server name (`namespace/name`, exactly one `/`) | | ||
| | `title` | `string` | no | Optional human-readable display name | | ||
| | `description` | `string` | yes | Human-readable explanation of server capabilities | | ||
| | `version` | `string` | yes | Server version (semver preferred; ranges rejected) | | ||
| | `websiteUrl` | `string` | no | Homepage / docs / project website URL | | ||
| | `repository` | [`McpServerRepository`](../src/types.ts#L176) | no | Source repository metadata | | ||
| | `remotes` | [`McpRegistryRemote`](../src/types.ts#L161)[] | no | Remote transports (`streamable-http` / `sse`) | | ||
| | `icons` | [`McpRegistryIcon`](../src/types.ts#L208)[] | no | UI icons | | ||
| | `packages` | [`McpRegistryPackage`](../src/types.ts#L230)[] | no | Installable package entries | | ||
| | `_meta` | [`McpServerMeta`](../src/types.ts#L260) | no | Reverse-DNS extension metadata | | ||
|
|
||
| ## [`McpServerRepository`](../src/types.ts#L176) | ||
|
|
||
| Repository metadata for browsing and cloning source (`Repository`). | ||
|
|
||
| | Field | Type | Required | Description | | ||
| | ----------- | -------- | -------- | ------------------------------------------------------------ | | ||
| | `url` | `string` | yes | Repository URL (web browse and git clone) | | ||
| | `source` | `string` | yes | Hosting service id (`github`, `gitlab`, `bitbucket`, …) | | ||
| | `id` | `string` | no | Hosting-service repo id (stable across renames) | | ||
| | `subfolder` | `string` | no | Clean relative path from repo root to the server (monorepos) | | ||
|
|
||
| ## [`McpRegistryRemote`](../src/types.ts#L161) | ||
|
|
||
| Remote transport entry (`RemoteTransport`): `streamable-http` or `sse`, plus | ||
| optional URL template variables. | ||
|
|
||
| | Field | Type | Required | Description | | ||
| | ----------- | -------------------------------------------------------------- | -------- | --------------------------------- | | ||
| | `type` | `'streamable-http' \| 'sse'` | yes | Remote transport kind | | ||
| | `url` | `string` | yes | Endpoint URL template | | ||
| | `headers` | [`McpKeyValueInput`](../src/types.ts#L62)[] | no | Optional HTTP headers | | ||
| | `variables` | `Record<string, McpInput>` ([`McpInput`](../src/types.ts#L26)) | no | URL template variable definitions | | ||
|
|
||
| ## [`McpRegistryIcon`](../src/types.ts#L208) | ||
|
|
||
| Icon resource for client UIs (`Icon`). | ||
|
|
||
| | Field | Type | Required | Description | | ||
| | ---------- | ------------------------------------------------------------------------------- | -------- | -------------------------------- | | ||
| | `src` | `string` | yes | URI of the icon resource | | ||
| | `mimeType` | `'image/png' \| 'image/jpeg' \| 'image/jpg' \| 'image/svg+xml' \| 'image/webp'` | no | MIME type override | | ||
| | `sizes` | `string[]` | no | Size hints (e.g. `48x48`, `any`) | | ||
| | `theme` | `'light' \| 'dark'` | no | Theme the icon is designed for | | ||
|
|
||
| ## [`McpRegistryPackage`](../src/types.ts#L230) | ||
|
|
||
| Installable package entry (`Package`). | ||
|
|
||
| | Field | Type | Required | Description | | ||
| | ---------------------- | ------------------------------------------- | -------- | ------------------------------------------------ | | ||
| | `registryType` | `string` | yes | Registry kind (`npm`, `pypi`, `cargo`, `oci`, …) | | ||
| | `identifier` | `string` | yes | Package name or download URL | | ||
| | `transport` | [`McpLocalTransport`](../src/types.ts#L147) | yes | Local / package transport config | | ||
| | `version` | `string` | no | Specific package version (no ranges) | | ||
| | `registryBaseUrl` | `string` | no | Base URL of the package registry | | ||
| | `runtimeHint` | `string` | no | Runtime hint (`npx`, `uvx`, `docker`, …) | | ||
| | `fileSha256` | `string` | no | SHA-256 of the package file | | ||
| | `environmentVariables` | [`McpKeyValueInput`](../src/types.ts#L62)[] | no | Environment variables for the package | | ||
| | `packageArguments` | [`McpArgument`](../src/types.ts#L101)[] | no | Arguments for the package binary | | ||
| | `runtimeArguments` | [`McpArgument`](../src/types.ts#L101)[] | no | Arguments for the runtime command | | ||
|
|
||
| ## [`McpServerMeta`](../src/types.ts#L260) | ||
|
|
||
| Extension metadata (`ServerDetail._meta`) with reverse-DNS keys. | ||
|
|
||
| | Field | Type | Required | Description | | ||
| | ----------------------------------------------------- | ------------------------- | -------- | -------------------------------------------- | | ||
| | `io.modelcontextprotocol.registry/publisher-provided` | `Record<string, unknown>` | no | Publisher metadata for downstream registries | | ||
| | `[key: string]` | `unknown` | no | Additional reverse-DNS namespaced extensions | | ||
|
|
||
| ## [`McpLocalTransport`](../src/types.ts#L147) | ||
|
|
||
| Local / package transport union (`LocalTransport`). | ||
|
|
||
| | Variant | `type` | Description | | ||
| | ---------------------------------------------------- | ------------------- | ---------------------------- | | ||
| | [`McpStdioTransport`](../src/types.ts#L109) | `'stdio'` | Stdio local transport | | ||
| | [`McpStreamableHttpTransport`](../src/types.ts#L119) | `'streamable-http'` | Streamable HTTP transport | | ||
| | [`McpSseTransport`](../src/types.ts#L133) | `'sse'` | Server-Sent Events transport | | ||
|
|
||
| ### [`McpStdioTransport`](../src/types.ts#L109) | ||
|
|
||
| | Field | Type | Required | Description | | ||
| | ------ | --------- | -------- | ----------- | | ||
| | `type` | `'stdio'` | yes | Literal | | ||
|
|
||
| ### [`McpStreamableHttpTransport`](../src/types.ts#L119) | ||
|
|
||
| | Field | Type | Required | Description | | ||
| | --------- | ------------------------------------------- | -------- | --------------------- | | ||
| | `type` | `'streamable-http'` | yes | Literal | | ||
| | `url` | `string` | yes | URL template | | ||
| | `headers` | [`McpKeyValueInput`](../src/types.ts#L62)[] | no | Optional HTTP headers | | ||
|
|
||
| ### [`McpSseTransport`](../src/types.ts#L133) | ||
|
|
||
| | Field | Type | Required | Description | | ||
| | --------- | ------------------------------------------- | -------- | ------------------------- | | ||
| | `type` | `'sse'` | yes | Literal | | ||
| | `url` | `string` | yes | SSE endpoint URL template | | ||
| | `headers` | [`McpKeyValueInput`](../src/types.ts#L62)[] | no | Optional HTTP headers | | ||
|
|
||
| ## [`McpInput`](../src/types.ts#L26) | ||
|
|
||
| Shared input leaf (`Input`) used by env vars, headers, variables, and arguments. | ||
|
|
||
| | Field | Type | Required | Description | | ||
| | ------------- | ------------------------------------------------- | -------- | -------------------------------------------- | | ||
| | `choices` | `string[]` | no | Allowed values the user must select from | | ||
| | `default` | `string` | no | Default value | | ||
| | `description` | `string` | no | Human-readable description for clients | | ||
| | `format` | `'string' \| 'number' \| 'boolean' \| 'filepath'` | no | Input format hint | | ||
| | `isRequired` | `boolean` | no | Whether the input is required | | ||
| | `isSecret` | `boolean` | no | Whether the input is a secret value | | ||
| | `placeholder` | `string` | no | Placeholder shown during configuration | | ||
| | `value` | `string` | no | Fixed value (end users should not configure) | | ||
|
|
||
| ## [`McpInputWithVariables`](../src/types.ts#L51) | ||
|
|
||
| Extends [`McpInput`](../src/types.ts#L26) with nested `{curly_brace}` variables | ||
| (`InputWithVariables`). | ||
|
|
||
| | Field | Type | Required | Description | | ||
| | ----------- | -------------------------------------------------------------- | -------- | --------------------------------- | | ||
| | `variables` | `Record<string, McpInput>` ([`McpInput`](../src/types.ts#L26)) | no | Nested variable input definitions | | ||
|
|
||
| ## [`McpKeyValueInput`](../src/types.ts#L62) | ||
|
|
||
| Named key/value input for env vars or headers (`KeyValueInput`). Extends | ||
| [`McpInputWithVariables`](../src/types.ts#L51). | ||
|
|
||
| | Field | Type | Required | Description | | ||
| | ------ | -------- | -------- | ----------------------------------- | | ||
| | `name` | `string` | yes | Header or environment variable name | | ||
|
|
||
| ## [`McpArgument`](../src/types.ts#L101) | ||
|
|
||
| Package or runtime argument union (`Argument`). | ||
|
|
||
| | Variant | `type` | Description | | ||
| | ---------------------------------------------- | -------------- | -------------------------------- | | ||
| | [`McpPositionalArgument`](../src/types.ts#L73) | `'positional'` | Positional command-line argument | | ||
| | [`McpNamedArgument`](../src/types.ts#L87) | `'named'` | Named flag (`--flag={value}`) | | ||
|
|
||
| ### [`McpPositionalArgument`](../src/types.ts#L73) | ||
|
|
||
| Extends [`McpInputWithVariables`](../src/types.ts#L51). | ||
|
|
||
| | Field | Type | Required | Description | | ||
| | ------------ | -------------- | -------- | ------------------------------------ | | ||
| | `type` | `'positional'` | yes | Literal | | ||
| | `isRepeated` | `boolean` | no | Whether the argument may be repeated | | ||
| | `valueHint` | `string` | no | Identifier / label for the argument | | ||
|
|
||
| ### [`McpNamedArgument`](../src/types.ts#L87) | ||
|
|
||
| Extends [`McpInputWithVariables`](../src/types.ts#L51). | ||
|
|
||
| | Field | Type | Required | Description | | ||
| | ------------ | --------- | -------- | ------------------------------------ | | ||
| | `type` | `'named'` | yes | Literal | | ||
| | `name` | `string` | yes | Flag name, including leading dashes | | ||
| | `isRepeated` | `boolean` | no | Whether the argument may be repeated | |
17 changes: 17 additions & 0 deletions
17
...tions/plugins/mcp-registry-server-mapping-common/examples/server-json/README.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| # Example `server.json` fixtures | ||
|
|
||
| Adapted from the MCP Registry **v1.8.1** | ||
| [generic server.json examples](https://github.com/modelcontextprotocol/registry/blob/v1.8.1/docs/reference/server-json/generic-server-json.md), | ||
| which conform to the | ||
| [`server.schema.json`](https://github.com/modelcontextprotocol/registry/blob/v1.8.1/docs/reference/server-json/draft/server.schema.json) | ||
| draft at that tag. | ||
|
|
||
| Structure matches the upstream examples; names, versions, URLs, and metadata | ||
| values are rewritten for local testing of the mapping library. | ||
|
|
||
| | File | Upstream example | | ||
| | ------------------------------ | ----------------------------------------- | | ||
| | `npm.server.json` | Basic server with NPM package | | ||
| | `npm-oci.server.json` | Filesystem server with npm + OCI packages | | ||
| | `nuget-positional.server.json` | Constant (fixed) package arguments | | ||
| | `remote.server.json` | Remote server | |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.