Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
406 changes: 394 additions & 12 deletions package-lock.json

Large diffs are not rendered by default.

36 changes: 31 additions & 5 deletions packages/cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ To install sourceloop-cli, run
```shell
npm install @sourceloop/cli
```

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:

## Usage
Expand All @@ -34,6 +35,7 @@ USAGE
* [`sl cdk`](#sl-cdk)
* [`sl extension [NAME]`](#sl-extension-name)
* [`sl help [COMMAND]`](#sl-help-command)
* [`sl mcp`](#sl-mcp)
* [`sl microservice [NAME]`](#sl-microservice-name)
* [`sl scaffold [NAME]`](#sl-scaffold-name)
* [`sl update`](#sl-update)
Expand Down Expand Up @@ -79,7 +81,7 @@ OPTIONS

-p, --packageJsonName=packageJsonName Package name for arc-cdk

-r, --relativePathToApp=relativePathToApp Relative path to the service you want to deploy
-r, --relativePathToApp=relativePathToApp Relative path to the application ts file

--help show manual pages
```
Expand All @@ -88,7 +90,7 @@ _See code: [src/commands/cdk.ts](https://github.com/sourcefuse/loopback4-microse

## `sl extension [NAME]`

add an extension
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.

```
USAGE
Expand Down Expand Up @@ -120,9 +122,33 @@ OPTIONS

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

## `sl mcp`

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.

```
USAGE
$ sl mcp

OPTIONS
--help show manual pages

DESCRIPTION
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.
You can use it using the following MCP server configuration:
"sourceloop": {
"command": "npx",
"args": ["@sourceloop/cli", "mcp"],
"timeout": 300
}
```

_See code: [src/commands/mcp.ts](https://github.com/sourcefuse/loopback4-microservice-catalog/blob/v10.0.0/src/commands/mcp.ts)_

## `sl microservice [NAME]`

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

```
USAGE
Expand Down Expand Up @@ -153,7 +179,7 @@ OPTIONS
Type of the datasource

--[no-]facade
Create as facade
Create as facade inside the facades folder

--help
show manual pages
Expand All @@ -169,7 +195,7 @@ _See code: [src/commands/microservice.ts](https://github.com/sourcefuse/loopback

## `sl scaffold [NAME]`

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

```
USAGE
Expand Down
3 changes: 2 additions & 1 deletion packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
"license": "MIT",
"dependencies": {
"@loopback/cli": "^5.2.1",
"@modelcontextprotocol/sdk": "^1.12.1",
"@oclif/command": "^1.8.16",
"@oclif/config": "^1.18.3",
"@oclif/plugin-autocomplete": "1.3.10",
Expand All @@ -55,7 +56,7 @@
"ts-morph": "^19.0.0",
"tslib": "^2.6.2",
"yeoman-environment": "^3.19.3",
"yeoman-generator": "^5.9.0",
"yeoman-generator": "^5.10.0",
"yosay": "^2.0.2"
},
"devDependencies": {
Expand Down
68 changes: 68 additions & 0 deletions packages/cli/src/__tests__/commands/mcp.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import {McpServer} from '@modelcontextprotocol/sdk/server/mcp';
import {Config} from '@oclif/config';
import {expect} from 'chai';
import {stub} from 'sinon';
import {Mcp} from '../../commands/mcp';
import {TestMCPCommand} from '../fixtures';
import {McpServerStub} from '../fixtures/mcp-service-stub';

describe('mcp', () => {
let command: Mcp;
let callStub: sinon.SinonStub;
let server: McpServerStub;
beforeEach(async () => {
command = new Mcp([], new Config({root: ''}), stub(), undefined, [
TestMCPCommand,
]);
callStub = stub();
server = new McpServerStub(callStub);
command.server = server as unknown as McpServer;

await command.run();
expect(callStub.callCount).to.eq(1);
});

afterEach(() => {
callStub.resetHistory();
});

it('should call tool with correct parameters', async () => {
const result = await server.callTool('TestMCPCommand', {
name: 'test',
description: 'This is a test command',
owner: 'test-owner',
cwd: '/test/cwd',
});
expect(JSON.parse(result.content[0].text)).to.deep.equal({
inputs: {
name: 'test',
description: 'This is a test command',
owner: 'test-owner',
cwd: '/test/cwd',
// not passed so default value is false for booleans
integrate: false,
},
command: 'test-mcp-command',
});
});

it('should call throw error for registered command if invalid payload is provided', async () => {
const result = await server.callTool('TestMCPCommand', {
name: 'test',
description: 'This is a test command',
owner: 'test-owner',
cwd: '/test/cwd',
});
expect(JSON.parse(result.content[0].text)).to.deep.equal({
inputs: {
name: 'test',
description: 'This is a test command',
owner: 'test-owner',
cwd: '/test/cwd',
// not passed so default value is false for booleans
integrate: false,
},
command: 'test-mcp-command',
});
});
});
2 changes: 2 additions & 0 deletions packages/cli/src/__tests__/fixtures/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './mcp-service-stub';
export * from './test-mcp-command';
51 changes: 51 additions & 0 deletions packages/cli/src/__tests__/fixtures/mcp-service-stub.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
// eslint-disable-next-line @typescript-eslint/naming-convention
import Sinon from 'sinon';
import {z} from 'zod';
import {AnyObject} from '../../types';

export class McpServerStub {
private callStub: Sinon.SinonStub;
constructor(callStub: Sinon.SinonStub) {
this.callStub = callStub;
}

private toolMap: Record<string, Function> = {};

tool(
name: string,
description: string,
params: Record<string, z.ZodTypeAny>,
run: (
args: Record<string, AnyObject[string]>,
) => Promise<AnyObject[string]>,
): void {
this.toolMap[name] = async (args: Record<string, AnyObject[string]>) => {
try {
const parsedArgs = z.object(params).parse(args);
return await run(parsedArgs);
} catch (error) {
if (error instanceof z.ZodError) {
throw new Error(`Validation error: ${error.message}`);
}
throw error;
}
};
}

callTool(
name: string,
args: Record<string, AnyObject[string]>,
): Promise<AnyObject[string]> {
if (!this.toolMap[name]) {
throw new Error(`Tool ${name} not found`);
}
return this.toolMap[name](args);
}

async connect() {
this.callStub({
type: 'connect',
message: 'MCP Server connected successfully',
});
}
}
55 changes: 55 additions & 0 deletions packages/cli/src/__tests__/fixtures/test-mcp-command.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
// Copyright (c) 2023 Sourcefuse Technologies
//
// This software is released under the MIT License.
// https://opensource.org/licenses/MIT
import {flags} from '@oclif/command';
import {AnyObject, McpTextResponse} from '../../types';

export class TestMCPCommand {
static readonly description = 'A dummy command to test the MCP functionality';
static readonly mcpDescription = `
This is a dummy command to test the MCP functionality.
`;

static readonly flags = {
help: flags.boolean({
name: 'help',
description: 'show manual pages',
type: 'boolean',
}),
cwd: flags.string({
name: 'working-directory',
description:
'Directory where project will be scaffolded, instead of the project name',
}),
owner: flags.string({
name: 'owner',
description: 'owner of the repo',
}),
description: flags.string({
name: 'description',
description: 'description of the repo',
}),
integrate: flags.boolean({
name: 'integrate',
description: 'Do you want to include integration files?',
}),
};
static readonly args = [
{name: 'name', description: 'name of the project', required: false},
];

static async mcpRun(inputs: AnyObject): Promise<McpTextResponse> {
return {
content: [
{
type: 'text',
text: JSON.stringify({
inputs,
command: 'test-mcp-command',
}),
},
],
};
}
}
2 changes: 0 additions & 2 deletions packages/cli/src/__tests__/helper/command-test.helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,6 @@ export function commandTest(testCase: CommandTestCase, command: ICommand) {
for (let i = 0; i < calls.length; i++) {
expect(calls[i].args[0][0]).to.be.deep.equal(testCase.prompts[i].input);
}
// get second argument of first call of env.run
expect(stubEnv.run.getCall(0).args[1]).is.deep.equal(testCase.options);
});
}

Expand Down
7 changes: 7 additions & 0 deletions packages/cli/src/__tests__/suite/mcp.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export const mcpSuite = [
{
name: 'mcp command without any option',
options: {},
prompts: [],
},
];
6 changes: 3 additions & 3 deletions packages/cli/src/__tests__/suite/microservice-prompts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export const microservicePromptsSuite = [
input: {
name: 'facade',
type: 'confirm',
message: 'Create as facade',
message: 'Create as facade inside the facades folder',
default: false,
},
output: true,
Expand Down Expand Up @@ -64,7 +64,7 @@ export const microservicePromptsSuite = [
input: {
name: 'facade',
type: 'confirm',
message: 'Create as facade',
message: 'Create as facade inside the facades folder',
default: false,
},
output: false,
Expand Down Expand Up @@ -160,7 +160,7 @@ export const microservicePromptsSuite = [
input: {
name: 'facade',
type: 'confirm',
message: 'Create as facade',
message: 'Create as facade inside the facades folder',
default: false,
},
output: false,
Expand Down
15 changes: 14 additions & 1 deletion packages/cli/src/app-generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,17 @@ import * as Generator from 'yeoman-generator';
/* eslint-enable @typescript-eslint/naming-convention */
export default class AppGenerator<
T extends Generator.GeneratorOptions,
> extends AppGeneratorLB4<T> {}
> extends AppGeneratorLB4<T> {
exitGeneration: string | Error = '';
exit(reason: string | Error) {
if (!reason) return;
this.exitGeneration = reason;
if (this.options.inMcp) {
if (reason instanceof Error) {
throw new Error(reason.message);
} else {
throw new Error(reason);
}
}
}
}
13 changes: 10 additions & 3 deletions packages/cli/src/base-generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export abstract class BaseGenerator<
T extends Generator.GeneratorOptions,
> extends Generator<T> {
root = '';
private exitGeneration?: string;
private exitGeneration: string | Error = '';

async copyTemplateAsync() {
const readdriAsync = promisify(readdir);
Expand Down Expand Up @@ -40,9 +40,16 @@ export abstract class BaseGenerator<
return super.destinationRoot(rootPath);
}

exit(reason?: string) {
if (reason) return;
exit(reason?: string | Error) {
if (!reason) return;
this.exitGeneration = reason;
if (this.options.inMcp) {
if (reason instanceof Error) {
throw new Error(reason.message);
} else {
throw new Error(reason);
}
}
}

shouldExit() {
Expand Down
Loading
Loading