Skip to content

Commit 17c1238

Browse files
akshatdubeysfAkshat Dubey
andauthored
feat(cli): add mcp server command (#2282)
* feat(cli): add mcp server GH-0 * feat(cli): add more details for arguments mcp description GH-0 * fix(cli): update descriptions GH-0 --------- Co-authored-by: Akshat Dubey <[email protected]>
1 parent 8725535 commit 17c1238

24 files changed

+1100
-40
lines changed

package-lock.json

Lines changed: 394 additions & 12 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/cli/README.md

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ To install sourceloop-cli, run
99
```shell
1010
npm install @sourceloop/cli
1111
```
12+
1213
Once the above command is executed, you will be able to access the CLI commands directly from your terminal. You can use either `sl` or `arc` as shorthand to run any of the `sourceloop` commands listed below. A sample usage is provided for reference:
1314

1415
## Usage
@@ -34,6 +35,7 @@ USAGE
3435
* [`sl cdk`](#sl-cdk)
3536
* [`sl extension [NAME]`](#sl-extension-name)
3637
* [`sl help [COMMAND]`](#sl-help-command)
38+
* [`sl mcp`](#sl-mcp)
3739
* [`sl microservice [NAME]`](#sl-microservice-name)
3840
* [`sl scaffold [NAME]`](#sl-scaffold-name)
3941
* [`sl update`](#sl-update)
@@ -79,7 +81,7 @@ OPTIONS
7981
8082
-p, --packageJsonName=packageJsonName Package name for arc-cdk
8183
82-
-r, --relativePathToApp=relativePathToApp Relative path to the service you want to deploy
84+
-r, --relativePathToApp=relativePathToApp Relative path to the application ts file
8385
8486
--help show manual pages
8587
```
@@ -88,7 +90,7 @@ _See code: [src/commands/cdk.ts](https://github.com/sourcefuse/loopback4-microse
8890

8991
## `sl extension [NAME]`
9092

91-
add an extension
93+
This generates a local package in the packages folder of a ARC generated monorepo. This package can then be installed and used inside other modules in the monorepo.
9294

9395
```
9496
USAGE
@@ -120,9 +122,33 @@ OPTIONS
120122

121123
_See code: [@oclif/plugin-help](https://github.com/oclif/plugin-help/blob/v3.2.18/src/commands/help.ts)_
122124

125+
## `sl mcp`
126+
127+
Command that runs an MCP server for the sourceloop CLI, this is not supposed to be run directly, but rather used by the MCP client to interact with the CLI commands.
128+
129+
```
130+
USAGE
131+
$ sl mcp
132+
133+
OPTIONS
134+
--help show manual pages
135+
136+
DESCRIPTION
137+
Command that runs an MCP server for the sourceloop CLI, this is not supposed to be run directly, but rather used by
138+
the MCP client to interact with the CLI commands.
139+
You can use it using the following MCP server configuration:
140+
"sourceloop": {
141+
"command": "npx",
142+
"args": ["@sourceloop/cli", "mcp"],
143+
"timeout": 300
144+
}
145+
```
146+
147+
_See code: [src/commands/mcp.ts](https://github.com/sourcefuse/loopback4-microservice-catalog/blob/v10.0.0/src/commands/mcp.ts)_
148+
123149
## `sl microservice [NAME]`
124150

125-
add a microservice
151+
Add a microservice in the services or facade folder of a ARC generated monorepo. This can also optionally add migrations for the same microservice.
126152

127153
```
128154
USAGE
@@ -153,7 +179,7 @@ OPTIONS
153179
Type of the datasource
154180
155181
--[no-]facade
156-
Create as facade
182+
Create as facade inside the facades folder
157183
158184
--help
159185
show manual pages
@@ -169,7 +195,7 @@ _See code: [src/commands/microservice.ts](https://github.com/sourcefuse/loopback
169195

170196
## `sl scaffold [NAME]`
171197

172-
create a project scaffold
198+
Setup a ARC based monorepo using npm workspaces with an empty services, facades and packages folder
173199

174200
```
175201
USAGE

packages/cli/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
"license": "MIT",
3939
"dependencies": {
4040
"@loopback/cli": "^5.2.1",
41+
"@modelcontextprotocol/sdk": "^1.12.1",
4142
"@oclif/command": "^1.8.16",
4243
"@oclif/config": "^1.18.3",
4344
"@oclif/plugin-autocomplete": "1.3.10",
@@ -55,7 +56,7 @@
5556
"ts-morph": "^19.0.0",
5657
"tslib": "^2.6.2",
5758
"yeoman-environment": "^3.19.3",
58-
"yeoman-generator": "^5.9.0",
59+
"yeoman-generator": "^5.10.0",
5960
"yosay": "^2.0.2"
6061
},
6162
"devDependencies": {
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import {McpServer} from '@modelcontextprotocol/sdk/server/mcp';
2+
import {Config} from '@oclif/config';
3+
import {expect} from 'chai';
4+
import {stub} from 'sinon';
5+
import {Mcp} from '../../commands/mcp';
6+
import {TestMCPCommand} from '../fixtures';
7+
import {McpServerStub} from '../fixtures/mcp-service-stub';
8+
9+
describe('mcp', () => {
10+
let command: Mcp;
11+
let callStub: sinon.SinonStub;
12+
let server: McpServerStub;
13+
beforeEach(async () => {
14+
command = new Mcp([], new Config({root: ''}), stub(), undefined, [
15+
TestMCPCommand,
16+
]);
17+
callStub = stub();
18+
server = new McpServerStub(callStub);
19+
command.server = server as unknown as McpServer;
20+
21+
await command.run();
22+
expect(callStub.callCount).to.eq(1);
23+
});
24+
25+
afterEach(() => {
26+
callStub.resetHistory();
27+
});
28+
29+
it('should call tool with correct parameters', async () => {
30+
const result = await server.callTool('TestMCPCommand', {
31+
name: 'test',
32+
description: 'This is a test command',
33+
owner: 'test-owner',
34+
cwd: '/test/cwd',
35+
});
36+
expect(JSON.parse(result.content[0].text)).to.deep.equal({
37+
inputs: {
38+
name: 'test',
39+
description: 'This is a test command',
40+
owner: 'test-owner',
41+
cwd: '/test/cwd',
42+
// not passed so default value is false for booleans
43+
integrate: false,
44+
},
45+
command: 'test-mcp-command',
46+
});
47+
});
48+
49+
it('should call throw error for registered command if invalid payload is provided', async () => {
50+
const result = await server.callTool('TestMCPCommand', {
51+
name: 'test',
52+
description: 'This is a test command',
53+
owner: 'test-owner',
54+
cwd: '/test/cwd',
55+
});
56+
expect(JSON.parse(result.content[0].text)).to.deep.equal({
57+
inputs: {
58+
name: 'test',
59+
description: 'This is a test command',
60+
owner: 'test-owner',
61+
cwd: '/test/cwd',
62+
// not passed so default value is false for booleans
63+
integrate: false,
64+
},
65+
command: 'test-mcp-command',
66+
});
67+
});
68+
});
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export * from './mcp-service-stub';
2+
export * from './test-mcp-command';
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
// eslint-disable-next-line @typescript-eslint/naming-convention
2+
import Sinon from 'sinon';
3+
import {z} from 'zod';
4+
import {AnyObject} from '../../types';
5+
6+
export class McpServerStub {
7+
private callStub: Sinon.SinonStub;
8+
constructor(callStub: Sinon.SinonStub) {
9+
this.callStub = callStub;
10+
}
11+
12+
private toolMap: Record<string, Function> = {};
13+
14+
tool(
15+
name: string,
16+
description: string,
17+
params: Record<string, z.ZodTypeAny>,
18+
run: (
19+
args: Record<string, AnyObject[string]>,
20+
) => Promise<AnyObject[string]>,
21+
): void {
22+
this.toolMap[name] = async (args: Record<string, AnyObject[string]>) => {
23+
try {
24+
const parsedArgs = z.object(params).parse(args);
25+
return await run(parsedArgs);
26+
} catch (error) {
27+
if (error instanceof z.ZodError) {
28+
throw new Error(`Validation error: ${error.message}`);
29+
}
30+
throw error;
31+
}
32+
};
33+
}
34+
35+
callTool(
36+
name: string,
37+
args: Record<string, AnyObject[string]>,
38+
): Promise<AnyObject[string]> {
39+
if (!this.toolMap[name]) {
40+
throw new Error(`Tool ${name} not found`);
41+
}
42+
return this.toolMap[name](args);
43+
}
44+
45+
async connect() {
46+
this.callStub({
47+
type: 'connect',
48+
message: 'MCP Server connected successfully',
49+
});
50+
}
51+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
// Copyright (c) 2023 Sourcefuse Technologies
2+
//
3+
// This software is released under the MIT License.
4+
// https://opensource.org/licenses/MIT
5+
import {flags} from '@oclif/command';
6+
import {AnyObject, McpTextResponse} from '../../types';
7+
8+
export class TestMCPCommand {
9+
static readonly description = 'A dummy command to test the MCP functionality';
10+
static readonly mcpDescription = `
11+
This is a dummy command to test the MCP functionality.
12+
`;
13+
14+
static readonly flags = {
15+
help: flags.boolean({
16+
name: 'help',
17+
description: 'show manual pages',
18+
type: 'boolean',
19+
}),
20+
cwd: flags.string({
21+
name: 'working-directory',
22+
description:
23+
'Directory where project will be scaffolded, instead of the project name',
24+
}),
25+
owner: flags.string({
26+
name: 'owner',
27+
description: 'owner of the repo',
28+
}),
29+
description: flags.string({
30+
name: 'description',
31+
description: 'description of the repo',
32+
}),
33+
integrate: flags.boolean({
34+
name: 'integrate',
35+
description: 'Do you want to include integration files?',
36+
}),
37+
};
38+
static readonly args = [
39+
{name: 'name', description: 'name of the project', required: false},
40+
];
41+
42+
static async mcpRun(inputs: AnyObject): Promise<McpTextResponse> {
43+
return {
44+
content: [
45+
{
46+
type: 'text',
47+
text: JSON.stringify({
48+
inputs,
49+
command: 'test-mcp-command',
50+
}),
51+
},
52+
],
53+
};
54+
}
55+
}

packages/cli/src/__tests__/helper/command-test.helper.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,6 @@ export function commandTest(testCase: CommandTestCase, command: ICommand) {
3636
for (let i = 0; i < calls.length; i++) {
3737
expect(calls[i].args[0][0]).to.be.deep.equal(testCase.prompts[i].input);
3838
}
39-
// get second argument of first call of env.run
40-
expect(stubEnv.run.getCall(0).args[1]).is.deep.equal(testCase.options);
4139
});
4240
}
4341

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export const mcpSuite = [
2+
{
3+
name: 'mcp command without any option',
4+
options: {},
5+
prompts: [],
6+
},
7+
];

packages/cli/src/__tests__/suite/microservice-prompts.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ export const microservicePromptsSuite = [
2222
input: {
2323
name: 'facade',
2424
type: 'confirm',
25-
message: 'Create as facade',
25+
message: 'Create as facade inside the facades folder',
2626
default: false,
2727
},
2828
output: true,
@@ -64,7 +64,7 @@ export const microservicePromptsSuite = [
6464
input: {
6565
name: 'facade',
6666
type: 'confirm',
67-
message: 'Create as facade',
67+
message: 'Create as facade inside the facades folder',
6868
default: false,
6969
},
7070
output: false,
@@ -160,7 +160,7 @@ export const microservicePromptsSuite = [
160160
input: {
161161
name: 'facade',
162162
type: 'confirm',
163-
message: 'Create as facade',
163+
message: 'Create as facade inside the facades folder',
164164
default: false,
165165
},
166166
output: false,

0 commit comments

Comments
 (0)