-
Notifications
You must be signed in to change notification settings - Fork 667
[mcp-server] Add more tools to mcp server #5215
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| // 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'; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,47 @@ | ||
| // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. | ||
| // See LICENSE in the project root for license information. | ||
|
|
||
| import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; | ||
| import { | ||
| type BaseTool, | ||
| RushConflictResolverTool, | ||
| RushMigrateProjectTool, | ||
| RushCommandValidatorTool, | ||
| RushWorkspaceDetailsTool, | ||
| RushProjectDetailsTool, | ||
| RushDocsTool | ||
| } from './tools'; | ||
|
|
||
| export class RushMCPServer extends McpServer { | ||
| private _rushWorkspacePath: string; | ||
| private _tools: BaseTool[] = []; | ||
|
|
||
| public constructor(rushWorkspacePath: string) { | ||
| super({ | ||
| name: 'rush', | ||
| version: '1.0.0' | ||
| }); | ||
|
|
||
| this._rushWorkspacePath = rushWorkspacePath; | ||
|
|
||
| this._initializeTools(); | ||
| this._registerTools(); | ||
| } | ||
|
|
||
| private _initializeTools(): void { | ||
| this._tools.push(new RushConflictResolverTool()); | ||
| this._tools.push(new RushMigrateProjectTool(this._rushWorkspacePath)); | ||
| this._tools.push(new RushCommandValidatorTool()); | ||
| this._tools.push(new RushWorkspaceDetailsTool()); | ||
| this._tools.push(new RushProjectDetailsTool()); | ||
| this._tools.push(new RushDocsTool()); | ||
| } | ||
|
|
||
| private _registerTools(): void { | ||
| process.chdir(this._rushWorkspacePath); | ||
|
|
||
| for (const tool of this._tools) { | ||
| tool.register(this); | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,2 +1,25 @@ | ||
| // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. | ||
| // See LICENSE in the project root for license information. | ||
|
|
||
| import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; | ||
|
|
||
| import { log } from './utilities/log'; | ||
| import { RushMCPServer } from './server'; | ||
|
|
||
| const main = async (): Promise<void> => { | ||
| const rushWorkspacePath: string | undefined = process.argv[2]; | ||
| if (!rushWorkspacePath) { | ||
| throw new Error('Please provide workspace root path as the first argument'); | ||
| } | ||
|
|
||
| const server: RushMCPServer = new RushMCPServer(rushWorkspacePath); | ||
| const transport: StdioServerTransport = new StdioServerTransport(); | ||
| await server.connect(transport); | ||
|
|
||
| log('Rush MCP Server running on stdio'); | ||
| }; | ||
|
|
||
| main().catch((error) => { | ||
| log('Fatal error running server:', error); | ||
| process.exit(1); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,59 @@ | ||
| // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. | ||
| // See LICENSE in the project root for license information. | ||
|
|
||
| import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp'; | ||
| import type { RequestHandlerExtra } from '@modelcontextprotocol/sdk/shared/protocol'; | ||
| import type { | ||
| CallToolResultSchema, | ||
| ServerNotification, | ||
| ServerRequest | ||
| } from '@modelcontextprotocol/sdk/types'; | ||
| import type { z, ZodRawShape, ZodTypeAny } from 'zod'; | ||
|
|
||
| export type CallToolResult = z.infer<typeof CallToolResultSchema>; | ||
|
|
||
| type ToolCallback<Args extends undefined | ZodRawShape = undefined> = Args extends ZodRawShape | ||
| ? ( | ||
| args: z.objectOutputType<Args, ZodTypeAny>, | ||
| extra: RequestHandlerExtra<ServerRequest, ServerNotification> | ||
| ) => CallToolResult | Promise<CallToolResult> | ||
| : ( | ||
| extra: RequestHandlerExtra<ServerRequest, ServerNotification> | ||
| ) => CallToolResult | Promise<CallToolResult>; | ||
|
|
||
| export interface IBaseToolOptions<Args extends ZodRawShape = ZodRawShape> { | ||
| name: string; | ||
| description: string; | ||
| schema: Args; | ||
| } | ||
|
|
||
| export abstract class BaseTool<Args extends ZodRawShape = ZodRawShape> { | ||
| private _options: IBaseToolOptions<Args>; | ||
|
|
||
| protected constructor(options: IBaseToolOptions<Args>) { | ||
| this._options = options; | ||
| } | ||
|
|
||
| protected abstract executeAsync(...args: Parameters<ToolCallback<Args>>): ReturnType<ToolCallback<Args>>; | ||
|
|
||
| public register(server: McpServer): void { | ||
| // TODO: remove ts-ignore | ||
| // @ts-ignore | ||
| server.tool(this._options.name, this._options.description, this._options.schema, async (...args) => { | ||
| try { | ||
| const result: CallToolResult = await this.executeAsync(...(args as Parameters<ToolCallback<Args>>)); | ||
| return result; | ||
| } catch (error: unknown) { | ||
| return { | ||
| isError: true, | ||
| content: [ | ||
| { | ||
| type: 'text', | ||
| text: error instanceof Error ? error.message : error | ||
| } | ||
| ] | ||
| }; | ||
| } | ||
| }); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,63 @@ | ||
| // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. | ||
| // See LICENSE in the project root for license information. | ||
|
|
||
| import { z } from 'zod'; | ||
| import type { RushConfiguration } from '@rushstack/rush-sdk'; | ||
| import type { IExecutableSpawnSyncOptions } from '@rushstack/node-core-library'; | ||
|
|
||
| import { CommandRunner } from '../utilities/command-runner'; | ||
| import { getRushConfiguration } from '../utilities/common'; | ||
| import { BaseTool, type CallToolResult } from './base.tool'; | ||
|
|
||
| export class RushConflictResolverTool extends BaseTool { | ||
| public constructor() { | ||
| super({ | ||
| name: 'rush_pnpm_lock_file_conflict_resolver', | ||
| description: | ||
| 'If a user requests to resolve a pnpm-lock.yaml file conflict, use this tool to automatically fix the conflict directly.', | ||
| schema: { | ||
| lockfilePath: z.string().describe('The path to the pnpm-lock.yaml file, should pass absolute path') | ||
| } | ||
| }); | ||
| } | ||
|
|
||
| private _tryGetSubspaceNameFromLockfilePath( | ||
| lockfilePath: string, | ||
| rushConfiguration: RushConfiguration | ||
| ): string | null { | ||
| for (const subspace of rushConfiguration.subspaces) { | ||
| const folderPath: string = subspace.getSubspaceConfigFolderPath(); | ||
| if (lockfilePath.startsWith(folderPath)) { | ||
| return subspace.subspaceName; | ||
| } | ||
| } | ||
| return null; | ||
| } | ||
|
|
||
| public async executeAsync({ lockfilePath }: { lockfilePath: string }): Promise<CallToolResult> { | ||
| const rushConfiguration: RushConfiguration = await getRushConfiguration(); | ||
| const subspaceName: string | null = this._tryGetSubspaceNameFromLockfilePath( | ||
| lockfilePath, | ||
| rushConfiguration | ||
| ); | ||
| if (!subspaceName) { | ||
| throw new Error('subspace name not found'); | ||
| } | ||
|
|
||
| const options: IExecutableSpawnSyncOptions = { | ||
| stdio: 'inherit', | ||
| currentWorkingDirectory: rushConfiguration.rushJsonFolder | ||
| }; | ||
| await CommandRunner.runGitCommandAsync(['checkout', '--theirs', lockfilePath], options); | ||
| await CommandRunner.runRushCommandAsync(['update', '--subspace', subspaceName], options); | ||
|
|
||
| return { | ||
| content: [ | ||
| { | ||
| type: 'text', | ||
| text: 'Conflict resolved successfully' | ||
| } | ||
| ] | ||
| }; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,51 @@ | ||
| // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. | ||
| // See LICENSE in the project root for license information. | ||
|
|
||
| import { z } from 'zod'; | ||
|
|
||
| import { BaseTool, type CallToolResult } from './base.tool'; | ||
|
|
||
| interface IDocsResult { | ||
| query: string; | ||
| results: { | ||
| score: number; | ||
| text: string; | ||
| }[]; | ||
| count: number; | ||
| searchTimeMs: number; | ||
| } | ||
|
|
||
| export class RushDocsTool extends BaseTool { | ||
| public constructor() { | ||
| super({ | ||
| name: 'rush_docs', | ||
| description: | ||
| 'Search and retrieve relevant sections from Rush official documentation based on user queries.', | ||
| schema: { | ||
| userQuery: z.string().describe('The user query to search for relevant documentation sections.') | ||
| } | ||
| }); | ||
| } | ||
|
|
||
| public async executeAsync({ userQuery }: { userQuery: string }): Promise<CallToolResult> { | ||
| // An example of a knowledge base that can run, but needs to be replaced with Microsoft’s service. | ||
| const response: Response = await fetch('http://47.120.46.115/search', { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We should change this demo so that it doesn't rely on a black box. If the code behind But for demo/example purposes, it probably would be sufficient to rewrite this function so that it replies with static responses, e.g. from a JSON file.
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Another idea would be to use the const https = require('https');
const query = 'dogs';
const url = `https://api.duckduckgo.com/?q=${encodeURIComponent(query)}&format=json`;
https.get(url, (res) => {
let data = '';
res.on('data', (chunk) => {
data += chunk;
});
res.on('end', () => {
try {
const json = JSON.parse(data);
console.log('Heading:', json.Heading);
console.log('Abstract:', json.Abstract);
console.log('Abstract URL:', json.AbstractURL);
if (json.RelatedTopics && json.RelatedTopics.length > 0) {
console.log('\nRelated Topics:');
json.RelatedTopics.forEach((topic, index) => {
if (topic.Text && topic.FirstURL) {
console.log(`${index + 1}. ${topic.Text}`);
console.log(topic.FirstURL);
}
});
}
} catch (e) {
console.error('Error parsing JSON:', e.message);
}
});
}).on('error', (err) => {
console.error('Request failed:', err.message);
});Output: |
||
| method: 'POST', | ||
| headers: { | ||
| 'Content-Type': 'application/json' | ||
| }, | ||
| body: JSON.stringify({ query: userQuery, topK: 10 }) | ||
| }); | ||
|
|
||
| const result: IDocsResult = (await response.json()) as IDocsResult; | ||
|
|
||
| return { | ||
| content: [ | ||
| { | ||
| type: 'text', | ||
| text: result.results.map((item) => item.text).join('\n') | ||
| } | ||
| ] | ||
| }; | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. | ||
| // See LICENSE in the project root for license information. | ||
|
|
||
| export * from './base.tool'; | ||
| export * from './migrate-project.tool'; | ||
| export * from './project-details.tool'; | ||
| export * from './rush-command-validator.tool'; | ||
| export * from './workspace-details'; | ||
| export * from './conflict-resolver.tool'; | ||
| export * from './docs.tool'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.