diff --git a/apps/rush-mcp-server/config/api-extractor.json b/apps/rush-mcp-server/config/api-extractor.json new file mode 100644 index 00000000000..996e271d3dd --- /dev/null +++ b/apps/rush-mcp-server/config/api-extractor.json @@ -0,0 +1,19 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json", + + "mainEntryPointFilePath": "/lib/index.d.ts", + + "apiReport": { + "enabled": true, + "reportFolder": "../../../common/reviews/api" + }, + + "docModel": { + "enabled": true, + "apiJsonFilePath": "../../../common/temp/api/.api.json" + }, + + "dtsRollup": { + "enabled": true + } +} diff --git a/apps/rush-mcp-server/package.json b/apps/rush-mcp-server/package.json index e8b14e82371..99922a6769f 100644 --- a/apps/rush-mcp-server/package.json +++ b/apps/rush-mcp-server/package.json @@ -2,6 +2,8 @@ "name": "@rushstack/mcp-server", "version": "0.1.4", "description": "A Model Context Protocol server implementation for Rush", + "main": "lib/index.js", + "typings": "dist/mcp-server.d.ts", "repository": { "type": "git", "url": "https://github.com/microsoft/rushstack.git", diff --git a/apps/rush-mcp-server/src/index.ts b/apps/rush-mcp-server/src/index.ts index 7dca0d0f5e9..20b52112e78 100644 --- a/apps/rush-mcp-server/src/index.ts +++ b/apps/rush-mcp-server/src/index.ts @@ -1,5 +1,12 @@ // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. // See LICENSE in the project root for license information. -export { log } from './utilities/log'; -export * from './tools'; +/** + * API for use by MCP plugins. + * @packageDocumentation + */ + +export * from './pluginFramework/IRushMcpPlugin'; +export * from './pluginFramework/IRushMcpTool'; +export { type IRegisterToolOptions, RushMcpPluginSession } from './pluginFramework/RushMcpPluginSession'; +export * from './pluginFramework/zodTypes'; diff --git a/apps/rush-mcp-server/src/pluginFramework/IRushMcpPlugin.ts b/apps/rush-mcp-server/src/pluginFramework/IRushMcpPlugin.ts new file mode 100644 index 00000000000..fa74680ccab --- /dev/null +++ b/apps/rush-mcp-server/src/pluginFramework/IRushMcpPlugin.ts @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import type { RushMcpPluginSession } from './RushMcpPluginSession'; + +/** + * MCP plugins should implement this interface. + * @public + */ +export interface IRushMcpPlugin { + onInitializeAsync(): Promise; +} + +/** + * The plugin's entry point should return this function as its default export. + * @public + */ +// eslint-disable-next-line @typescript-eslint/consistent-type-definitions +export type RushMcpPluginFactory = ( + session: RushMcpPluginSession, + configFile: TConfigFile | undefined +) => IRushMcpPlugin; diff --git a/apps/rush-mcp-server/src/pluginFramework/IRushMcpTool.ts b/apps/rush-mcp-server/src/pluginFramework/IRushMcpTool.ts new file mode 100644 index 00000000000..f6eaba91d70 --- /dev/null +++ b/apps/rush-mcp-server/src/pluginFramework/IRushMcpTool.ts @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import type * as zod from 'zod'; + +import type { CallToolResult } from './zodTypes'; + +/** + * MCP plugins should implement this interface. + * @public + */ +export interface IRushMcpTool { + readonly schema: TSchema; + executeAsync(input: zod.infer): Promise; +} diff --git a/apps/rush-mcp-server/src/pluginFramework/RushMcpPluginLoader.ts b/apps/rush-mcp-server/src/pluginFramework/RushMcpPluginLoader.ts new file mode 100644 index 00000000000..88dec926982 --- /dev/null +++ b/apps/rush-mcp-server/src/pluginFramework/RushMcpPluginLoader.ts @@ -0,0 +1,171 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import * as path from 'path'; +import { FileSystem, Import, JsonFile, JsonSchema } from '@rushstack/node-core-library'; +import { Autoinstaller } from '@rushstack/rush-sdk/lib/logic/Autoinstaller'; +import { RushGlobalFolder } from '@rushstack/rush-sdk/lib/api/RushGlobalFolder'; +import { RushConfiguration } from '@rushstack/rush-sdk/lib/api/RushConfiguration'; + +import type { IRushMcpPlugin, RushMcpPluginFactory } from './IRushMcpPlugin'; +import { RushMcpPluginSessionInternal } from './RushMcpPluginSession'; + +import rushMcpJsonSchemaObject from '../schemas/rush-mcp.schema.json'; +import rushMcpPluginSchemaObject from '../schemas/rush-mcp-plugin.schema.json'; + +/** + * Configuration for @rushstack/mcp-server in a monorepo. + * Corresponds to the contents of common/config/rush-mcp/rush-mcp.json + */ +export interface IJsonRushMcpConfig { + /** + * The list of plugins that @rushstack/mcp-server should load when processing this monorepo. + */ + mcpPlugins: IJsonRushMcpPlugin[]; +} + +/** + * Describes a single MCP plugin entry. + */ +export interface IJsonRushMcpPlugin { + /** + * The name of an NPM package that appears in the package.json "dependencies" for the autoinstaller. + */ + packageName: string; + + /** + * The name of a Rush autoinstaller with this package as its dependency. + * @rushstack/mcp-server will ensure this folder is installed before loading the plugin. + */ + autoinstaller: string; +} + +/** + * Manifest file for a Rush MCP plugin. + * Every plugin package must contain a "rush-mcp-plugin.json" manifest in the top-level folder. + */ +export interface IJsonRushMcpPluginManifest { + /** + * A name that uniquely identifies your plugin. + * Generally this should match the NPM package name; two plugins with the same pluginName cannot be loaded together. + */ + pluginName: string; + + /** + * Optional. Indicates that your plugin accepts a config file. + * The MCP server will load this schema file and provide it to the plugin. + * Path is typically `/common/config/rush-mcp/.json`. + */ + configFileSchema?: string; + + /** + * The module path to the plugin's entry point. + * Its default export must be a class implementing the MCP plugin interface. + */ + entryPoint: string; +} + +export class RushMcpPluginLoader { + private static readonly _rushMcpJsonSchema: JsonSchema = + JsonSchema.fromLoadedObject(rushMcpJsonSchemaObject); + private static readonly _rushMcpPluginSchemaObject: JsonSchema = + JsonSchema.fromLoadedObject(rushMcpPluginSchemaObject); + + private readonly _rushWorkspacePath: string; + + public constructor(rushWorkspacePath: string) { + this._rushWorkspacePath = rushWorkspacePath; + } + + public async loadAsync(): Promise { + const rushMcpFilePath: string = path.join( + this._rushWorkspacePath, + 'common/config/rush-mcp/rush-mcp.json' + ); + + if (!(await FileSystem.existsAsync(rushMcpFilePath))) { + // Should we report an error here? + return; + } + + const rushConfiguration: RushConfiguration = RushConfiguration.loadFromDefaultLocation({ + startingFolder: this._rushWorkspacePath + }); + + const jsonRushMcpConfig: IJsonRushMcpConfig = await JsonFile.loadAndValidateAsync( + rushMcpFilePath, + RushMcpPluginLoader._rushMcpJsonSchema + ); + + if (jsonRushMcpConfig.mcpPlugins.length === 0) { + return; + } + + const rushGlobalFolder: RushGlobalFolder = new RushGlobalFolder(); + + for (const jsonMcpPlugin of jsonRushMcpConfig.mcpPlugins) { + // Ensure the autoinstaller is installed + const autoinstaller: Autoinstaller = new Autoinstaller({ + autoinstallerName: jsonMcpPlugin.autoinstaller, + rushConfiguration, + rushGlobalFolder, + restrictConsoleOutput: false + }); + await autoinstaller.prepareAsync(); + + // Load the manifest + + // Suppose the autoinstaller is "my-autoinstaller" and the package is "rush-mcp-example-plugin". + // Then the folder will be: + // "/path/to/my-repo/common/autoinstallers/my-autoinstaller/node_modules/rush-mcp-example-plugin" + const installedPluginPackageFolder: string = await Import.resolvePackageAsync({ + baseFolderPath: autoinstaller.folderFullPath, + packageName: jsonMcpPlugin.packageName + }); + + const manifestFilePath: string = path.join(installedPluginPackageFolder, 'rush-mcp-plugin.json'); + if (!(await FileSystem.existsAsync(manifestFilePath))) { + throw new Error( + 'The "rush-mcp-plugin.json" manifest file was not found under ' + installedPluginPackageFolder + ); + } + + const jsonManifest: IJsonRushMcpPluginManifest = await JsonFile.loadAndValidateAsync( + manifestFilePath, + RushMcpPluginLoader._rushMcpPluginSchemaObject + ); + + // TODO: Load and validate config file if defined by the manifest + + const fullEntryPointPath: string = path.join(installedPluginPackageFolder, jsonManifest.entryPoint); + let pluginFactory: RushMcpPluginFactory; + try { + const entryPointModule: { default?: RushMcpPluginFactory } = require(fullEntryPointPath); + if (entryPointModule.default === undefined) { + throw new Error('The commonJS "default" export is missing'); + } + pluginFactory = entryPointModule.default; + } catch (e) { + throw new Error(`Unable to load plugin entry point at ${fullEntryPointPath}: ` + e.toString()); + } + + const session: RushMcpPluginSessionInternal = new RushMcpPluginSessionInternal(); + + let plugin: IRushMcpPlugin; + try { + // TODO: Replace "{}" with the plugin's parsed config file JSON + plugin = pluginFactory(session, {}); + } catch (e) { + throw new Error(`Error invoking entry point for plugin ${jsonManifest.pluginName}:` + e.toString()); + } + + try { + await plugin.onInitializeAsync(); + } catch (e) { + throw new Error( + `Error occurred in onInitializeAsync() for plugin ${jsonManifest.pluginName}:` + e.toString() + ); + } + } + } +} diff --git a/apps/rush-mcp-server/src/pluginFramework/RushMcpPluginSession.ts b/apps/rush-mcp-server/src/pluginFramework/RushMcpPluginSession.ts new file mode 100644 index 00000000000..8353d1d3e1e --- /dev/null +++ b/apps/rush-mcp-server/src/pluginFramework/RushMcpPluginSession.ts @@ -0,0 +1,36 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import type { IRushMcpTool } from './IRushMcpTool'; +import * as zod from 'zod'; +import type { zodModule } from './zodTypes'; + +/** + * Each plugin gets its own session. + * + * @public + */ +export interface IRegisterToolOptions { + toolName: string; + description?: string; +} + +/** + * Each plugin gets its own session. + * + * @public + */ +export abstract class RushMcpPluginSession { + public readonly zod: typeof zodModule = zod; + public abstract registerTool(options: IRegisterToolOptions, tool: IRushMcpTool): void; +} + +export class RushMcpPluginSessionInternal extends RushMcpPluginSession { + public constructor() { + super(); + } + + public override registerTool(options: IRegisterToolOptions, tool: IRushMcpTool): void { + // TODO: Register the tool + } +} diff --git a/apps/rush-mcp-server/src/pluginFramework/zodTypes.ts b/apps/rush-mcp-server/src/pluginFramework/zodTypes.ts new file mode 100644 index 00000000000..19bf22a2f12 --- /dev/null +++ b/apps/rush-mcp-server/src/pluginFramework/zodTypes.ts @@ -0,0 +1,14 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import type * as zod from 'zod'; +export type { zod as zodModule }; + +import { CallToolResultSchema } from '@modelcontextprotocol/sdk/types'; + +export { CallToolResultSchema }; + +/** + * @public + */ +export type CallToolResult = zod.infer; diff --git a/apps/rush-mcp-server/src/schemas/rush-mcp-plugin.schema.json b/apps/rush-mcp-server/src/schemas/rush-mcp-plugin.schema.json new file mode 100644 index 00000000000..e8e8757d9f1 --- /dev/null +++ b/apps/rush-mcp-server/src/schemas/rush-mcp-plugin.schema.json @@ -0,0 +1,21 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Rush MCP Plugin Manifest", + "type": "object", + "properties": { + "pluginName": { + "type": "string", + "description": "A name that uniquely identifies your plugin. Generally this should match the NPM package name; two plugins with the same pluginName cannot be loaded together." + }, + "configFileSchema": { + "type": "string", + "description": "Optional. Indicates that your plugin accepts a config file. The MCP server will load this schema file and provide it to the plugin. Path is typically `/common/config/rush-mcp/.json`." + }, + "entryPoint": { + "type": "string", + "description": "The module path to the plugin's entry point. Its default export must be a class implementing the MCP plugin interface." + } + }, + "required": ["pluginName", "entryPoint"], + "additionalProperties": false +} diff --git a/apps/rush-mcp-server/src/schemas/rush-mcp.schema.json b/apps/rush-mcp-server/src/schemas/rush-mcp.schema.json new file mode 100644 index 00000000000..0dd9a9546fe --- /dev/null +++ b/apps/rush-mcp-server/src/schemas/rush-mcp.schema.json @@ -0,0 +1,28 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "rush-mcp.json Configuration Schema", + "type": "object", + "properties": { + "mcpPlugins": { + "type": "array", + "description": "The list of plugins that `@rushstack/mcp-server` should load when processing this monorepo.", + "items": { + "type": "object", + "properties": { + "packageName": { + "type": "string", + "description": "The name of an NPM package that appears in the package.json \"dependencies\" for the autoinstaller." + }, + "autoinstaller": { + "type": "string", + "description": "The name of a Rush autoinstaller with this package as its dependency." + } + }, + "required": ["packageName", "autoinstaller"], + "additionalProperties": false + } + } + }, + "required": ["mcpPlugins"], + "additionalProperties": false +} diff --git a/apps/rush-mcp-server/src/server.ts b/apps/rush-mcp-server/src/server.ts index 9e4b7e0ced3..cd1d8d7d607 100644 --- a/apps/rush-mcp-server/src/server.ts +++ b/apps/rush-mcp-server/src/server.ts @@ -11,10 +11,12 @@ import { RushProjectDetailsTool, RushDocsTool } from './tools'; +import { RushMcpPluginLoader } from './pluginFramework/RushMcpPluginLoader'; export class RushMCPServer extends McpServer { private _rushWorkspacePath: string; private _tools: BaseTool[] = []; + private _pluginLoader: RushMcpPluginLoader; public constructor(rushWorkspacePath: string) { super({ @@ -23,9 +25,14 @@ export class RushMCPServer extends McpServer { }); this._rushWorkspacePath = rushWorkspacePath; + this._pluginLoader = new RushMcpPluginLoader(this._rushWorkspacePath); + } + public async startAsync(): Promise { this._initializeTools(); this._registerTools(); + + await this._pluginLoader.loadAsync(); } private _initializeTools(): void { diff --git a/apps/rush-mcp-server/src/start.ts b/apps/rush-mcp-server/src/start.ts index 93ab38d285f..c9f1e85a26b 100644 --- a/apps/rush-mcp-server/src/start.ts +++ b/apps/rush-mcp-server/src/start.ts @@ -13,6 +13,7 @@ const main = async (): Promise => { } const server: RushMCPServer = new RushMCPServer(rushWorkspacePath); + await server.startAsync(); const transport: StdioServerTransport = new StdioServerTransport(); await server.connect(transport); diff --git a/build-tests/rush-mcp-example-plugin/.eslintrc.js b/build-tests/rush-mcp-example-plugin/.eslintrc.js new file mode 100644 index 00000000000..de794c04ae0 --- /dev/null +++ b/build-tests/rush-mcp-example-plugin/.eslintrc.js @@ -0,0 +1,13 @@ +// This is a workaround for https://github.com/eslint/eslint/issues/3458 +require('local-eslint-config/patch/modern-module-resolution'); +// This is a workaround for https://github.com/microsoft/rushstack/issues/3021 +require('local-eslint-config/patch/custom-config-package-names'); + +module.exports = { + extends: [ + 'local-eslint-config/profile/node', + 'local-eslint-config/mixins/friendly-locals', + 'local-eslint-config/mixins/tsdoc' + ], + parserOptions: { tsconfigRootDir: __dirname } +}; diff --git a/build-tests/rush-mcp-example-plugin/.npmignore b/build-tests/rush-mcp-example-plugin/.npmignore new file mode 100644 index 00000000000..dc4a664618b --- /dev/null +++ b/build-tests/rush-mcp-example-plugin/.npmignore @@ -0,0 +1,35 @@ +# THIS IS A STANDARD TEMPLATE FOR .npmignore FILES IN THIS REPO. + +# Ignore all files by default, to avoid accidentally publishing unintended files. +* + +# Use negative patterns to bring back the specific things we want to publish. +!/bin/** +!/lib/** +!/lib-*/** +!/dist/** + +!CHANGELOG.md +!CHANGELOG.json +!heft-plugin.json +!rush-plugin-manifest.json +!ThirdPartyNotice.txt + +# Ignore certain patterns that should not get published. +/dist/*.stats.* +/lib/**/test/ +/lib-*/**/test/ +*.test.js + +# NOTE: These don't need to be specified, because NPM includes them automatically. +# +# package.json +# README.md +# LICENSE + +# --------------------------------------------------------------------------- +# DO NOT MODIFY ABOVE THIS LINE! Add any project-specific overrides below. +# --------------------------------------------------------------------------- + +!rush-mcp-plugin.json +!*.schema.json diff --git a/build-tests/rush-mcp-example-plugin/LICENSE b/build-tests/rush-mcp-example-plugin/LICENSE new file mode 100644 index 00000000000..5ad10fc49f8 --- /dev/null +++ b/build-tests/rush-mcp-example-plugin/LICENSE @@ -0,0 +1,24 @@ +rush-mcp-example-plugin + +Copyright (c) Microsoft Corporation. All rights reserved. + +MIT License + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/build-tests/rush-mcp-example-plugin/README.md b/build-tests/rush-mcp-example-plugin/README.md new file mode 100644 index 00000000000..8ca3190b2fc --- /dev/null +++ b/build-tests/rush-mcp-example-plugin/README.md @@ -0,0 +1,3 @@ +# rush-mcp-example-plugin + +This example project shows how to create a plugin for `@rushstack/mcp-server` diff --git a/build-tests/rush-mcp-example-plugin/config/rig.json b/build-tests/rush-mcp-example-plugin/config/rig.json new file mode 100644 index 00000000000..165ffb001f5 --- /dev/null +++ b/build-tests/rush-mcp-example-plugin/config/rig.json @@ -0,0 +1,7 @@ +{ + // The "rig.json" file directs tools to look for their config files in an external package. + // Documentation for this system: https://www.npmjs.com/package/@rushstack/rig-package + "$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json", + + "rigPackageName": "local-node-rig" +} diff --git a/build-tests/rush-mcp-example-plugin/package.json b/build-tests/rush-mcp-example-plugin/package.json new file mode 100644 index 00000000000..2f16f0b0044 --- /dev/null +++ b/build-tests/rush-mcp-example-plugin/package.json @@ -0,0 +1,18 @@ +{ + "name": "rush-mcp-example-plugin", + "version": "0.0.0", + "private": true, + "description": "Example showing how to create a plugin for @rushstack/mcp-server", + "license": "MIT", + "scripts": { + "build": "heft build --clean", + "_phase:build": "heft run --only build -- --clean" + }, + "dependencies": {}, + "devDependencies": { + "@rushstack/heft": "workspace:*", + "@rushstack/mcp-server": "workspace:*", + "local-node-rig": "workspace:*", + "local-eslint-config": "workspace:*" + } +} diff --git a/build-tests/rush-mcp-example-plugin/rush-mcp-plugin.json b/build-tests/rush-mcp-example-plugin/rush-mcp-plugin.json new file mode 100644 index 00000000000..48ff8a67fb9 --- /dev/null +++ b/build-tests/rush-mcp-example-plugin/rush-mcp-plugin.json @@ -0,0 +1,24 @@ +/** + * Every plugin package must contain a "rush-mcp-plugin.json" manifest in the top-level folder + * (next to package.json). + */ +{ + /** + * A name that uniquely identifies your plugin. Generally this should be the same name as + * the NPM package. If two NPM packages have the same pluginName, they cannot be loaded together. + */ + "pluginName": "rush-mcp-example-plugin", + + /** + * (OPTIONAL) Indicates that your plugin accepts a config file. The MCP server will load this + * file and provide it to the plugin. + * + * The config file path will be `/common/config/rush-mcp/.json`. + */ + "configFileSchema": "./lib/rush-mcp-example-plugin.schema.json", + + /** + * The entry point, whose default export should be a class that implements + */ + "entryPoint": "./lib/index.js" +} diff --git a/build-tests/rush-mcp-example-plugin/src/ExamplePlugin.ts b/build-tests/rush-mcp-example-plugin/src/ExamplePlugin.ts new file mode 100644 index 00000000000..d706e8c9a52 --- /dev/null +++ b/build-tests/rush-mcp-example-plugin/src/ExamplePlugin.ts @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import type { IRushMcpPlugin, RushMcpPluginSession } from '@rushstack/mcp-server'; +import { StateCapitalTool } from './StateCapitalTool'; + +export interface IExamplePluginConfigFile { + capitalsByState: Record; +} + +export class ExamplePlugin implements IRushMcpPlugin { + public session: RushMcpPluginSession; + public configFile: IExamplePluginConfigFile | undefined = undefined; + + public constructor(session: RushMcpPluginSession, configFile: IExamplePluginConfigFile | undefined) { + this.session = session; + this.configFile = configFile; + } + + public async onInitializeAsync(): Promise { + this.session.registerTool({ toolName: 'state_capital' }, new StateCapitalTool(this)); + } +} diff --git a/build-tests/rush-mcp-example-plugin/src/StateCapitalTool.ts b/build-tests/rush-mcp-example-plugin/src/StateCapitalTool.ts new file mode 100644 index 00000000000..f6ad8ce6812 --- /dev/null +++ b/build-tests/rush-mcp-example-plugin/src/StateCapitalTool.ts @@ -0,0 +1,41 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import type { IRushMcpTool, RushMcpPluginSession, CallToolResult, zodModule } from '@rushstack/mcp-server'; + +import type { ExamplePlugin } from './ExamplePlugin'; + +export class StateCapitalTool implements IRushMcpTool { + public readonly plugin: ExamplePlugin; + public readonly session: RushMcpPluginSession; + + public constructor(plugin: ExamplePlugin) { + this.plugin = plugin; + this.session = plugin.session; + } + + // ZOD relies on type inference generate a messy expression in the .d.ts file + // eslint-disable-next-line @typescript-eslint/explicit-function-return-type + public get schema() { + const zod: typeof zodModule = this.session.zod; + + return zod.object({ + state: zod.string().describe('The name of the state, in all lowercase') + }); + } + + public async executeAsync(input: zodModule.infer): Promise { + const capital: string | undefined = this.plugin.configFile?.capitalsByState[input.state]; + + return { + content: [ + { + type: 'text', + text: capital + ? `The capital of "${input.state}" is "${capital}"` + : `Unable to determine the answer from the data set.` + } + ] + }; + } +} diff --git a/build-tests/rush-mcp-example-plugin/src/index.ts b/build-tests/rush-mcp-example-plugin/src/index.ts new file mode 100644 index 00000000000..8866a23e566 --- /dev/null +++ b/build-tests/rush-mcp-example-plugin/src/index.ts @@ -0,0 +1,14 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import type { RushMcpPluginSession, RushMcpPluginFactory } from '@rushstack/mcp-server'; +import { ExamplePlugin, type IExamplePluginConfigFile } from './ExamplePlugin'; + +function createPlugin( + session: RushMcpPluginSession, + configFile: IExamplePluginConfigFile | undefined +): ExamplePlugin { + return new ExamplePlugin(session, configFile); +} + +export default createPlugin satisfies RushMcpPluginFactory; diff --git a/build-tests/rush-mcp-example-plugin/src/rush-mcp-example-plugin.schema.json b/build-tests/rush-mcp-example-plugin/src/rush-mcp-example-plugin.schema.json new file mode 100644 index 00000000000..534b3291d31 --- /dev/null +++ b/build-tests/rush-mcp-example-plugin/src/rush-mcp-example-plugin.schema.json @@ -0,0 +1,19 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "State Capital Map", + "type": "object", + "required": ["capitalsByState"], + "properties": { + "$schema": { + "type": "string" + }, + "capitalsByState": { + "type": "object", + "additionalProperties": { + "type": "string" + }, + "description": "A mapping of US state names (lowercase) to their capital cities." + } + }, + "additionalProperties": false +} diff --git a/build-tests/rush-mcp-example-plugin/tsconfig.json b/build-tests/rush-mcp-example-plugin/tsconfig.json new file mode 100644 index 00000000000..dac21d04081 --- /dev/null +++ b/build-tests/rush-mcp-example-plugin/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "./node_modules/local-node-rig/profiles/default/tsconfig-base.json" +} diff --git a/common/config/rush/nonbrowser-approved-packages.json b/common/config/rush/nonbrowser-approved-packages.json index bdcbd6cf572..c75ff273af2 100644 --- a/common/config/rush/nonbrowser-approved-packages.json +++ b/common/config/rush/nonbrowser-approved-packages.json @@ -222,6 +222,10 @@ "name": "@rushstack/lookup-by-path", "allowedCategories": [ "libraries" ] }, + { + "name": "@rushstack/mcp-server", + "allowedCategories": [ "tests" ] + }, { "name": "@rushstack/module-minifier", "allowedCategories": [ "libraries", "tests" ] diff --git a/common/config/subspaces/default/pnpm-lock.yaml b/common/config/subspaces/default/pnpm-lock.yaml index 104c481a2f4..4738fcd4d96 100644 --- a/common/config/subspaces/default/pnpm-lock.yaml +++ b/common/config/subspaces/default/pnpm-lock.yaml @@ -2216,6 +2216,21 @@ importers: specifier: workspace:* version: link:../../rigs/local-node-rig + ../../../build-tests/rush-mcp-example-plugin: + devDependencies: + '@rushstack/heft': + specifier: workspace:* + version: link:../../apps/heft + '@rushstack/mcp-server': + specifier: workspace:* + version: link:../../apps/rush-mcp-server + local-eslint-config: + specifier: workspace:* + version: link:../../eslint/local-eslint-config + local-node-rig: + specifier: workspace:* + version: link:../../rigs/local-node-rig + ../../../build-tests/rush-project-change-analyzer-test: dependencies: '@microsoft/rush-lib': diff --git a/common/reviews/api/mcp-server.api.md b/common/reviews/api/mcp-server.api.md new file mode 100644 index 00000000000..cecf5940f2a --- /dev/null +++ b/common/reviews/api/mcp-server.api.md @@ -0,0 +1,50 @@ +## API Report File for "@rushstack/mcp-server" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import { CallToolResultSchema } from '@modelcontextprotocol/sdk/types'; +import type * as zodModule from 'zod'; + +// @public (undocumented) +export type CallToolResult = zodModule.infer; + +export { CallToolResultSchema } + +// @public +export interface IRegisterToolOptions { + // (undocumented) + description?: string; + // (undocumented) + toolName: string; +} + +// @public +export interface IRushMcpPlugin { + // (undocumented) + onInitializeAsync(): Promise; +} + +// @public +export interface IRushMcpTool { + // (undocumented) + executeAsync(input: zodModule.infer): Promise; + // (undocumented) + readonly schema: TSchema; +} + +// @public +export type RushMcpPluginFactory = (session: RushMcpPluginSession, configFile: TConfigFile | undefined) => IRushMcpPlugin; + +// @public +export abstract class RushMcpPluginSession { + // (undocumented) + abstract registerTool(options: IRegisterToolOptions, tool: IRushMcpTool): void; + // (undocumented) + readonly zod: typeof zodModule; +} + +export { zodModule } + +``` diff --git a/rush.json b/rush.json index e977144bc85..7fdb8ad7792 100644 --- a/rush.json +++ b/rush.json @@ -660,6 +660,12 @@ "reviewCategory": "tests", "shouldPublish": false }, + { + "packageName": "rush-mcp-example-plugin", + "projectFolder": "build-tests/rush-mcp-example-plugin", + "reviewCategory": "tests", + "shouldPublish": false + }, { "packageName": "run-scenarios-helpers", "projectFolder": "build-tests/run-scenarios-helpers",