From 2003dcb4dbe57066dea2245a4b44abd12a9a3ebf Mon Sep 17 00:00:00 2001 From: benjdlambert Date: Mon, 18 May 2026 19:06:34 +0200 Subject: [PATCH] refactor(catalog-model): move mcp-server model to opt-in backend module Moves the mcp-server specType registration out of the default catalog entity model into a separate backend module following the same pattern as the AiResource module. Types and validators are now alpha exports. Signed-off-by: benjdlambert --- .changeset/mcp-server-model-module.md | 5 ++ packages/catalog-model/report-alpha.api.md | 33 ++++++++++++ packages/catalog-model/report.api.md | 30 ----------- .../src/kinds/McpServerApiEntity.ts | 8 +-- .../src/kinds/apiEntityModel.test.ts | 6 ++- packages/catalog-model/src/kinds/index.ts | 5 -- .../src/model/defaultCatalogEntityModel.ts | 2 - .../.eslintrc.js | 1 + .../catalog-info.yaml | 10 ++++ .../package.json | 53 +++++++++++++++++++ .../report.api.md | 11 ++++ .../src/index.ts | 23 ++++++++ .../src/module.ts | 44 +++++++++++++++ yarn.lock | 12 +++++ 14 files changed, 201 insertions(+), 42 deletions(-) create mode 100644 .changeset/mcp-server-model-module.md create mode 100644 plugins/catalog-backend-module-mcp-server-model/.eslintrc.js create mode 100644 plugins/catalog-backend-module-mcp-server-model/catalog-info.yaml create mode 100644 plugins/catalog-backend-module-mcp-server-model/package.json create mode 100644 plugins/catalog-backend-module-mcp-server-model/report.api.md create mode 100644 plugins/catalog-backend-module-mcp-server-model/src/index.ts create mode 100644 plugins/catalog-backend-module-mcp-server-model/src/module.ts diff --git a/.changeset/mcp-server-model-module.md b/.changeset/mcp-server-model-module.md new file mode 100644 index 0000000000..273e03d142 --- /dev/null +++ b/.changeset/mcp-server-model-module.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend-module-mcp-server-model': patch +--- + +New backend module that registers support for the `mcp-server` API spec type in the catalog. Install with `backend.add(import('@backstage/plugin-catalog-backend-module-mcp-server-model'))`. diff --git a/packages/catalog-model/report-alpha.api.md b/packages/catalog-model/report-alpha.api.md index b2861ea5c9..c00f6ac675 100644 --- a/packages/catalog-model/report-alpha.api.md +++ b/packages/catalog-model/report-alpha.api.md @@ -491,6 +491,11 @@ export const isAiResourceEntity: ( entity: Entity, ) => entity is AiResourceEntityV1alpha1; +// @alpha +export function isMcpServerApiEntity( + entity: Entity, +): entity is McpServerApiEntity; + // @alpha export const isSkillAiResourceEntity: ( entity: Entity, @@ -501,6 +506,34 @@ export type KindValidator = { check(entity: Entity): Promise; }; +// @alpha +export interface McpServerApiEntity extends Entity { + // (undocumented) + apiVersion: 'backstage.io/v1alpha1' | 'backstage.io/v1beta1'; + // (undocumented) + kind: 'API'; + // (undocumented) + spec: { + type: 'mcp-server'; + lifecycle: string; + owner: string; + system?: string; + remotes: McpServerRemote[]; + }; +} + +// @alpha +export const mcpServerApiEntityModel: CatalogModelLayer; + +// @alpha +export const mcpServerApiEntityValidator: KindValidator; + +// @alpha +export type McpServerRemote = { + type: string; + url: string; +}; + // @alpha export interface SkillAiResourceEntityV1alpha1 extends Entity { // (undocumented) diff --git a/packages/catalog-model/report.api.md b/packages/catalog-model/report.api.md index be42c33e7c..ec03ce41e1 100644 --- a/packages/catalog-model/report.api.md +++ b/packages/catalog-model/report.api.md @@ -273,11 +273,6 @@ export function isLocationEntity( entity: Entity, ): entity is LocationEntityV1alpha1; -// @public -export function isMcpServerApiEntity( - entity: Entity, -): entity is McpServerApiEntity; - // @public (undocumented) export function isResourceEntity( entity: Entity, @@ -337,31 +332,6 @@ export const locationEntityV1alpha1Validator: KindValidator; // @public export function makeValidator(overrides?: Partial): Validators; -// @public -export interface McpServerApiEntity extends Entity { - // (undocumented) - apiVersion: 'backstage.io/v1alpha1' | 'backstage.io/v1beta1'; - // (undocumented) - kind: 'API'; - // (undocumented) - spec: { - type: 'mcp-server'; - lifecycle: string; - owner: string; - system?: string; - remotes: McpServerRemote[]; - }; -} - -// @public -export const mcpServerApiEntityValidator: KindValidator; - -// @public -export type McpServerRemote = { - type: string; - url: string; -}; - // @public export class NoForeignRootFieldsEntityPolicy implements EntityPolicy { constructor(knownFields?: string[]); diff --git a/packages/catalog-model/src/kinds/McpServerApiEntity.ts b/packages/catalog-model/src/kinds/McpServerApiEntity.ts index 4d6ea662f7..a81de0cc41 100644 --- a/packages/catalog-model/src/kinds/McpServerApiEntity.ts +++ b/packages/catalog-model/src/kinds/McpServerApiEntity.ts @@ -23,7 +23,7 @@ import { ajvCompiledJsonSchemaValidator } from './util'; * An MCP (Model Context Protocol) server represented as an API entity * (spec.type: 'mcp-server'). * - * @public + * @alpha */ export interface McpServerApiEntity extends Entity { apiVersion: 'backstage.io/v1alpha1' | 'backstage.io/v1beta1'; @@ -40,7 +40,7 @@ export interface McpServerApiEntity extends Entity { /** * A transport endpoint for an MCP server. * - * @public + * @alpha */ export type McpServerRemote = { type: string; @@ -50,7 +50,7 @@ export type McpServerRemote = { /** * {@link KindValidator} for the `mcp-server` specType of API entities. * - * @public + * @alpha */ export const mcpServerApiEntityValidator = ajvCompiledJsonSchemaValidator(mcpServerSchema); @@ -58,7 +58,7 @@ export const mcpServerApiEntityValidator = /** * Type guard: narrows an entity to the MCP server API subtype. * - * @public + * @alpha */ export function isMcpServerApiEntity( entity: Entity, diff --git a/packages/catalog-model/src/kinds/apiEntityModel.test.ts b/packages/catalog-model/src/kinds/apiEntityModel.test.ts index 3df75fdd53..09373490fd 100644 --- a/packages/catalog-model/src/kinds/apiEntityModel.test.ts +++ b/packages/catalog-model/src/kinds/apiEntityModel.test.ts @@ -16,9 +16,13 @@ import { compileCatalogModel } from '../model/compileCatalogModel'; import { defaultCatalogEntityModel } from '../model/defaultCatalogEntityModel'; +import { mcpServerApiEntityModel } from './McpServerApiEntity'; describe('apiEntityModel mcp-server dispatch', () => { - const model = compileCatalogModel([defaultCatalogEntityModel]); + const model = compileCatalogModel([ + defaultCatalogEntityModel, + mcpServerApiEntityModel, + ]); it('routes mcp-server and non-mcp-server v1alpha1 entities to different schemas', () => { const mcp = model.getKind({ diff --git a/packages/catalog-model/src/kinds/index.ts b/packages/catalog-model/src/kinds/index.ts index d8b70832f0..674e4eef30 100644 --- a/packages/catalog-model/src/kinds/index.ts +++ b/packages/catalog-model/src/kinds/index.ts @@ -19,11 +19,6 @@ export type { ApiEntityV1alpha1 as ApiEntity, ApiEntityV1alpha1, } from './ApiEntityV1alpha1'; -export { - isMcpServerApiEntity, - mcpServerApiEntityValidator, -} from './McpServerApiEntity'; -export type { McpServerApiEntity, McpServerRemote } from './McpServerApiEntity'; export { componentEntityV1alpha1Validator } from './ComponentEntityV1alpha1'; export type { ComponentEntityV1alpha1 as ComponentEntity, diff --git a/packages/catalog-model/src/model/defaultCatalogEntityModel.ts b/packages/catalog-model/src/model/defaultCatalogEntityModel.ts index 2d0c1f7f7b..f997a78707 100644 --- a/packages/catalog-model/src/model/defaultCatalogEntityModel.ts +++ b/packages/catalog-model/src/model/defaultCatalogEntityModel.ts @@ -15,7 +15,6 @@ */ import { apiEntityModel } from '../kinds/ApiEntityV1alpha1'; -import { mcpServerApiEntityModel } from '../kinds/McpServerApiEntity'; import { componentEntityModel } from '../kinds/ComponentEntityV1alpha1'; import { domainEntityModel } from '../kinds/DomainEntityV1alpha1'; import { groupEntityModel } from '../kinds/GroupEntityV1alpha1'; @@ -37,7 +36,6 @@ export const defaultCatalogEntityModel = createCatalogModelLayer({ layerId: 'catalog.backstage.io/default-entity-model', builder: model => { model.import(apiEntityModel); - model.import(mcpServerApiEntityModel); model.import(componentEntityModel); model.import(domainEntityModel); model.import(groupEntityModel); diff --git a/plugins/catalog-backend-module-mcp-server-model/.eslintrc.js b/plugins/catalog-backend-module-mcp-server-model/.eslintrc.js new file mode 100644 index 0000000000..e2a53a6ad2 --- /dev/null +++ b/plugins/catalog-backend-module-mcp-server-model/.eslintrc.js @@ -0,0 +1 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/catalog-backend-module-mcp-server-model/catalog-info.yaml b/plugins/catalog-backend-module-mcp-server-model/catalog-info.yaml new file mode 100644 index 0000000000..5ea5697284 --- /dev/null +++ b/plugins/catalog-backend-module-mcp-server-model/catalog-info.yaml @@ -0,0 +1,10 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-plugin-catalog-backend-module-mcp-server-model + title: '@backstage/plugin-catalog-backend-module-mcp-server-model' + description: Adds support for the mcp-server API spec type to the catalog backend plugin. +spec: + lifecycle: experimental + type: backstage-backend-plugin-module + owner: maintainers diff --git a/plugins/catalog-backend-module-mcp-server-model/package.json b/plugins/catalog-backend-module-mcp-server-model/package.json new file mode 100644 index 0000000000..d074f7d488 --- /dev/null +++ b/plugins/catalog-backend-module-mcp-server-model/package.json @@ -0,0 +1,53 @@ +{ + "name": "@backstage/plugin-catalog-backend-module-mcp-server-model", + "version": "0.1.0", + "description": "Adds support for the mcp-server API spec type to the catalog backend plugin.", + "backstage": { + "role": "backend-plugin-module", + "pluginId": "catalog", + "pluginPackage": "@backstage/plugin-catalog-backend" + }, + "publishConfig": { + "access": "public" + }, + "repository": { + "type": "git", + "url": "https://github.com/backstage/backstage", + "directory": "plugins/catalog-backend-module-mcp-server-model" + }, + "license": "Apache-2.0", + "exports": { + ".": "./src/index.ts", + "./package.json": "./package.json" + }, + "main": "src/index.ts", + "types": "src/index.ts", + "typesVersions": { + "*": { + "package.json": [ + "package.json" + ] + } + }, + "files": [ + "dist" + ], + "scripts": { + "build": "backstage-cli package build", + "clean": "backstage-cli package clean", + "lint": "backstage-cli package lint", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack", + "start": "backstage-cli package start", + "test": "backstage-cli package test" + }, + "dependencies": { + "@backstage/backend-plugin-api": "workspace:^", + "@backstage/catalog-model": "workspace:^", + "@backstage/plugin-catalog-node": "workspace:^" + }, + "devDependencies": { + "@backstage/backend-test-utils": "workspace:^", + "@backstage/cli": "workspace:^" + } +} diff --git a/plugins/catalog-backend-module-mcp-server-model/report.api.md b/plugins/catalog-backend-module-mcp-server-model/report.api.md new file mode 100644 index 0000000000..a3f872c435 --- /dev/null +++ b/plugins/catalog-backend-module-mcp-server-model/report.api.md @@ -0,0 +1,11 @@ +## API Report File for "@backstage/plugin-catalog-backend-module-mcp-server-model" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import { BackendFeature } from '@backstage/backend-plugin-api'; + +// @public +const catalogModuleMcpServerModel: BackendFeature; +export default catalogModuleMcpServerModel; +``` diff --git a/plugins/catalog-backend-module-mcp-server-model/src/index.ts b/plugins/catalog-backend-module-mcp-server-model/src/index.ts new file mode 100644 index 0000000000..2f3e927e16 --- /dev/null +++ b/plugins/catalog-backend-module-mcp-server-model/src/index.ts @@ -0,0 +1,23 @@ +/* + * Copyright 2026 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Adds support for the mcp-server API spec type to the catalog backend plugin. + * + * @packageDocumentation + */ + +export { catalogModuleMcpServerModel as default } from './module'; diff --git a/plugins/catalog-backend-module-mcp-server-model/src/module.ts b/plugins/catalog-backend-module-mcp-server-model/src/module.ts new file mode 100644 index 0000000000..0a9c1eca00 --- /dev/null +++ b/plugins/catalog-backend-module-mcp-server-model/src/module.ts @@ -0,0 +1,44 @@ +/* + * Copyright 2026 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { createBackendModule } from '@backstage/backend-plugin-api'; +import { + CatalogModelSources, + mcpServerApiEntityModel, +} from '@backstage/catalog-model/alpha'; +import { catalogModelExtensionPoint } from '@backstage/plugin-catalog-node/alpha'; + +/** + * Registers support for the mcp-server API spec type in the catalog. + * + * @public + */ +export const catalogModuleMcpServerModel = createBackendModule({ + pluginId: 'catalog', + moduleId: 'mcp-server-model', + register(reg) { + reg.registerInit({ + deps: { + model: catalogModelExtensionPoint, + }, + async init({ model }) { + model.addModelSource( + CatalogModelSources.static([mcpServerApiEntityModel]), + ); + }, + }); + }, +}); diff --git a/yarn.lock b/yarn.lock index 6655516238..5df632d1bf 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5068,6 +5068,18 @@ __metadata: languageName: unknown linkType: soft +"@backstage/plugin-catalog-backend-module-mcp-server-model@workspace:plugins/catalog-backend-module-mcp-server-model": + version: 0.0.0-use.local + resolution: "@backstage/plugin-catalog-backend-module-mcp-server-model@workspace:plugins/catalog-backend-module-mcp-server-model" + dependencies: + "@backstage/backend-plugin-api": "workspace:^" + "@backstage/backend-test-utils": "workspace:^" + "@backstage/catalog-model": "workspace:^" + "@backstage/cli": "workspace:^" + "@backstage/plugin-catalog-node": "workspace:^" + languageName: unknown + linkType: soft + "@backstage/plugin-catalog-backend-module-msgraph-incremental@workspace:plugins/catalog-backend-module-msgraph-incremental": version: 0.0.0-use.local resolution: "@backstage/plugin-catalog-backend-module-msgraph-incremental@workspace:plugins/catalog-backend-module-msgraph-incremental"