feat(mcp): add MCP endpoint; fix TODOs (pkg.json name/version, NestJS Logger)
This commit is contained in:
@@ -1,7 +1,5 @@
|
||||
NODE_ENV=development
|
||||
PORT=3000
|
||||
APP_NAME=liquio-qa-bot
|
||||
APP_VERSION=1.0.0
|
||||
|
||||
# Directory (relative to cwd) containing *.json key descriptors
|
||||
KEYS_DIR=keys
|
||||
|
||||
Generated
+154
-6
@@ -9,6 +9,7 @@
|
||||
"version": "1.0.0",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "^1.29.0",
|
||||
"@mozilla/readability": "^0.6.0",
|
||||
"@nestjs/common": "^11.1.18",
|
||||
"@nestjs/config": "^4.0.3",
|
||||
@@ -24,7 +25,8 @@
|
||||
"reflect-metadata": "^0.2.2",
|
||||
"rxjs": "^7.8.2",
|
||||
"swagger-ui-express": "^5.0.1",
|
||||
"typeorm": "^0.3.28"
|
||||
"typeorm": "^0.3.28",
|
||||
"zod": "^4.3.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nestjs/cli": "^11.0.17",
|
||||
@@ -400,6 +402,18 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@hono/node-server": {
|
||||
"version": "1.19.13",
|
||||
"resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.13.tgz",
|
||||
"integrity": "sha512-TsQLe4i2gvoTtrHje625ngThGBySOgSK3Xo2XRYOdqGN1teR8+I7vchQC46uLJi8OF62YTYA3AhSpumtkhsaKQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18.14.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"hono": "^4"
|
||||
}
|
||||
},
|
||||
"node_modules/@inquirer/ansi": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-1.0.2.tgz",
|
||||
@@ -922,6 +936,46 @@
|
||||
"integrity": "sha512-xgAyonlVVS+q7Vc7qLW0UrJU7rSFcETRWsqdXZtjzRU8dF+6CkozTK4V4y1LwOX7j8r/vHphjDeMeGI4tNGeGA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@modelcontextprotocol/sdk": {
|
||||
"version": "1.29.0",
|
||||
"resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.29.0.tgz",
|
||||
"integrity": "sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@hono/node-server": "^1.19.9",
|
||||
"ajv": "^8.17.1",
|
||||
"ajv-formats": "^3.0.1",
|
||||
"content-type": "^1.0.5",
|
||||
"cors": "^2.8.5",
|
||||
"cross-spawn": "^7.0.5",
|
||||
"eventsource": "^3.0.2",
|
||||
"eventsource-parser": "^3.0.0",
|
||||
"express": "^5.2.1",
|
||||
"express-rate-limit": "^8.2.1",
|
||||
"hono": "^4.11.4",
|
||||
"jose": "^6.1.3",
|
||||
"json-schema-typed": "^8.0.2",
|
||||
"pkce-challenge": "^5.0.0",
|
||||
"raw-body": "^3.0.0",
|
||||
"zod": "^3.25 || ^4.0",
|
||||
"zod-to-json-schema": "^3.25.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@cfworker/json-schema": "^4.1.1",
|
||||
"zod": "^3.25 || ^4.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@cfworker/json-schema": {
|
||||
"optional": true
|
||||
},
|
||||
"zod": {
|
||||
"optional": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@mozilla/readability": {
|
||||
"version": "0.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@mozilla/readability/-/readability-0.6.0.tgz",
|
||||
@@ -1650,7 +1704,6 @@
|
||||
"version": "8.18.0",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz",
|
||||
"integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
@@ -1667,7 +1720,6 @@
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz",
|
||||
"integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ajv": "^8.0.0"
|
||||
@@ -2811,6 +2863,27 @@
|
||||
"node": ">=0.8.x"
|
||||
}
|
||||
},
|
||||
"node_modules/eventsource": {
|
||||
"version": "3.0.7",
|
||||
"resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz",
|
||||
"integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"eventsource-parser": "^3.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/eventsource-parser": {
|
||||
"version": "3.0.6",
|
||||
"resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.6.tgz",
|
||||
"integrity": "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/expand-template": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz",
|
||||
@@ -2863,11 +2936,28 @@
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/express-rate-limit": {
|
||||
"version": "8.3.2",
|
||||
"resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.3.2.tgz",
|
||||
"integrity": "sha512-77VmFeJkO0/rvimEDuUC5H30oqUC4EyOhyGccfqoLebB0oiEYfM7nwPrsDsBL1gsTpwfzX8SFy2MT3TDyRq+bg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ip-address": "10.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 16"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/express-rate-limit"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"express": ">= 4.11"
|
||||
}
|
||||
},
|
||||
"node_modules/fast-deep-equal": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
||||
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/fast-json-stable-stringify": {
|
||||
@@ -2887,7 +2977,6 @@
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz",
|
||||
"integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
@@ -3269,6 +3358,15 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/hono": {
|
||||
"version": "4.12.12",
|
||||
"resolved": "https://registry.npmjs.org/hono/-/hono-4.12.12.tgz",
|
||||
"integrity": "sha512-p1JfQMKaceuCbpJKAPKVqyqviZdS0eUxH9v82oWo1kb9xjQ5wA6iP3FNVAPDFlz5/p7d45lO+BpSk1tuSZMF4Q==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=16.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/html-encoding-sniffer": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz",
|
||||
@@ -3366,6 +3464,15 @@
|
||||
"integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/ip-address": {
|
||||
"version": "10.1.0",
|
||||
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz",
|
||||
"integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 12"
|
||||
}
|
||||
},
|
||||
"node_modules/ipaddr.js": {
|
||||
"version": "1.9.1",
|
||||
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
|
||||
@@ -3520,6 +3627,15 @@
|
||||
"url": "https://github.com/chalk/supports-color?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/jose": {
|
||||
"version": "6.2.2",
|
||||
"resolved": "https://registry.npmjs.org/jose/-/jose-6.2.2.tgz",
|
||||
"integrity": "sha512-d7kPDd34KO/YnzaDOlikGpOurfF0ByC2sEV4cANCtdqLlTfBlw2p14O/5d/zv40gJPbIQxfES3nSx1/oYNyuZQ==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/panva"
|
||||
}
|
||||
},
|
||||
"node_modules/js-tokens": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||
@@ -3590,9 +3706,14 @@
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
|
||||
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/json-schema-typed": {
|
||||
"version": "8.0.2",
|
||||
"resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz",
|
||||
"integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==",
|
||||
"license": "BSD-2-Clause"
|
||||
},
|
||||
"node_modules/json5": {
|
||||
"version": "2.2.3",
|
||||
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
|
||||
@@ -4197,6 +4318,15 @@
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/pkce-challenge": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.1.tgz",
|
||||
"integrity": "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=16.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/playwright": {
|
||||
"version": "1.59.1",
|
||||
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.59.1.tgz",
|
||||
@@ -6041,6 +6171,24 @@
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/zod": {
|
||||
"version": "4.3.6",
|
||||
"resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz",
|
||||
"integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
}
|
||||
},
|
||||
"node_modules/zod-to-json-schema": {
|
||||
"version": "3.25.2",
|
||||
"resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.2.tgz",
|
||||
"integrity": "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==",
|
||||
"license": "ISC",
|
||||
"peerDependencies": {
|
||||
"zod": "^3.25.28 || ^4"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+3
-1
@@ -13,6 +13,7 @@
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "^1.29.0",
|
||||
"@mozilla/readability": "^0.6.0",
|
||||
"@nestjs/common": "^11.1.18",
|
||||
"@nestjs/config": "^4.0.3",
|
||||
@@ -28,7 +29,8 @@
|
||||
"reflect-metadata": "^0.2.2",
|
||||
"rxjs": "^7.8.2",
|
||||
"swagger-ui-express": "^5.0.1",
|
||||
"typeorm": "^0.3.28"
|
||||
"typeorm": "^0.3.28",
|
||||
"zod": "^4.3.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nestjs/cli": "^11.0.17",
|
||||
|
||||
@@ -7,6 +7,7 @@ import { BrowserModule } from './browser/browser.module';
|
||||
import { SessionEntity } from './session/session.entity';
|
||||
import { EnvironmentEntity } from './environment/environment.entity';
|
||||
import { EnvironmentModule } from './environment/environment.module';
|
||||
import { McpModule } from './mcp/mcp.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@@ -26,6 +27,7 @@ import { EnvironmentModule } from './environment/environment.module';
|
||||
AuthModule,
|
||||
BrowserModule,
|
||||
EnvironmentModule,
|
||||
McpModule,
|
||||
],
|
||||
controllers: [HealthController],
|
||||
})
|
||||
|
||||
@@ -7,5 +7,6 @@ import { SessionModule } from '../session/session.module';
|
||||
imports: [SessionModule],
|
||||
controllers: [BrowserController],
|
||||
providers: [BrowserService],
|
||||
exports: [BrowserService],
|
||||
})
|
||||
export class BrowserModule {}
|
||||
|
||||
+11
-8
@@ -1,31 +1,34 @@
|
||||
import { NestFactory } from '@nestjs/core';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { ValidationPipe } from '@nestjs/common';
|
||||
import { Logger, ValidationPipe } from '@nestjs/common';
|
||||
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
|
||||
import { AppModule } from './app.module';
|
||||
import { name as pkgName, version as pkgVersion } from '../package.json';
|
||||
|
||||
async function bootstrap() {
|
||||
const logger = new Logger('Bootstrap');
|
||||
const app = await NestFactory.create(AppModule);
|
||||
|
||||
app.useGlobalPipes(new ValidationPipe({ transform: true }));
|
||||
|
||||
const config = app.get(ConfigService);
|
||||
const port = config.get<number>('PORT', 3000);
|
||||
const appName = config.get<string>('APP_NAME', 'liquio-qa-bot');
|
||||
const appVersion = config.get<string>('APP_VERSION', '1.0.0');
|
||||
const nodeEnv = config.get<string>('NODE_ENV', 'development');
|
||||
|
||||
const swaggerConfig = new DocumentBuilder()
|
||||
.setTitle(appName)
|
||||
.setDescription(`${appName} API`)
|
||||
.setVersion(appVersion)
|
||||
.setTitle(pkgName)
|
||||
.setDescription(`${pkgName} API`)
|
||||
.setVersion(pkgVersion)
|
||||
.build();
|
||||
|
||||
const document = SwaggerModule.createDocument(app, swaggerConfig);
|
||||
SwaggerModule.setup('api', app, document);
|
||||
|
||||
await app.listen(port);
|
||||
console.log(`Application running on port ${port}`);
|
||||
console.log(`Swagger UI available at http://localhost:${port}/api`);
|
||||
|
||||
logger.log(`Application "${pkgName}" v${pkgVersion} running on port ${port} [${nodeEnv}]`);
|
||||
logger.log(`Swagger UI available at http://localhost:${port}/api`);
|
||||
logger.log(`MCP endpoint available at http://localhost:${port}/mcp`);
|
||||
}
|
||||
|
||||
bootstrap();
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import { All, Controller, Req, Res } from '@nestjs/common';
|
||||
import type { Request, Response } from 'express';
|
||||
import { McpService } from './mcp.service';
|
||||
|
||||
@Controller('mcp')
|
||||
export class McpController {
|
||||
constructor(private readonly mcpService: McpService) {}
|
||||
|
||||
@All()
|
||||
handle(@Req() req: Request, @Res() res: Response): Promise<void> {
|
||||
return this.mcpService.handle(req, res);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { McpController } from './mcp.controller';
|
||||
import { McpService } from './mcp.service';
|
||||
import { AuthModule } from '../auth/auth.module';
|
||||
import { SessionModule } from '../session/session.module';
|
||||
import { EnvironmentModule } from '../environment/environment.module';
|
||||
import { BrowserModule } from '../browser/browser.module';
|
||||
|
||||
@Module({
|
||||
imports: [AuthModule, SessionModule, EnvironmentModule, BrowserModule],
|
||||
controllers: [McpController],
|
||||
providers: [McpService],
|
||||
})
|
||||
export class McpModule {}
|
||||
@@ -0,0 +1,217 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
||||
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
||||
import { z } from 'zod';
|
||||
import type { Request, Response } from 'express';
|
||||
import { AuthService } from '../auth/auth.service';
|
||||
import { SessionService } from '../session/session.service';
|
||||
import { EnvironmentService } from '../environment/environment.service';
|
||||
import type { EnvironmentUrls } from '../environment/environment.entity';
|
||||
import { BrowserService } from '../browser/browser.service';
|
||||
|
||||
@Injectable()
|
||||
export class McpService {
|
||||
constructor(
|
||||
private readonly authService: AuthService,
|
||||
private readonly sessionService: SessionService,
|
||||
private readonly environmentService: EnvironmentService,
|
||||
private readonly browserService: BrowserService,
|
||||
) {}
|
||||
|
||||
async handle(req: Request, res: Response): Promise<void> {
|
||||
const server = new McpServer({ name: 'liquio-qa-bot', version: '1.0.0' });
|
||||
|
||||
// ── Auth ──────────────────────────────────────────────────────────────────
|
||||
|
||||
server.registerTool(
|
||||
'list_keys',
|
||||
{ description: 'List available key identifiers from the keys directory' },
|
||||
async () => {
|
||||
const keys = this.authService.listKeys();
|
||||
return { content: [{ type: 'text' as const, text: JSON.stringify(keys) }] };
|
||||
},
|
||||
);
|
||||
|
||||
server.registerTool(
|
||||
'login',
|
||||
{
|
||||
description: 'Log in using a file key against a named environment and store the session',
|
||||
inputSchema: {
|
||||
key: z.string().describe('Key identifier (filename without extension from keys/ dir)'),
|
||||
environmentName: z.string().describe('Environment name to resolve login/cabinet URLs'),
|
||||
sessionName: z.string().optional().describe('Session name to store credentials under. Auto-UUID if omitted.'),
|
||||
},
|
||||
},
|
||||
async ({ key, environmentName, sessionName }) => {
|
||||
const result = await this.authService.login(key, environmentName, sessionName);
|
||||
return { content: [{ type: 'text' as const, text: JSON.stringify(result) }] };
|
||||
},
|
||||
);
|
||||
|
||||
// ── Sessions ──────────────────────────────────────────────────────────────
|
||||
|
||||
server.registerTool(
|
||||
'list_sessions',
|
||||
{ description: 'List all stored sessions (id, sessionName, createdAt, updatedAt)' },
|
||||
async () => {
|
||||
const sessions = await this.sessionService.findAll();
|
||||
return { content: [{ type: 'text' as const, text: JSON.stringify(sessions) }] };
|
||||
},
|
||||
);
|
||||
|
||||
server.registerTool(
|
||||
'delete_session',
|
||||
{
|
||||
description: 'Delete a session by numeric ID',
|
||||
inputSchema: {
|
||||
id: z.number().int().describe('Session ID to delete'),
|
||||
},
|
||||
},
|
||||
async ({ id }) => {
|
||||
const sessions = await this.sessionService.findAll();
|
||||
if (!sessions.find(s => s.id === id)) {
|
||||
return { isError: true, content: [{ type: 'text' as const, text: `Session ${id} not found` }] };
|
||||
}
|
||||
await this.sessionService.remove(id);
|
||||
return { content: [{ type: 'text' as const, text: `Session ${id} deleted` }] };
|
||||
},
|
||||
);
|
||||
|
||||
// ── Environments ──────────────────────────────────────────────────────────
|
||||
|
||||
server.registerTool(
|
||||
'list_environments',
|
||||
{ description: 'List all environments' },
|
||||
async () => {
|
||||
const envs = await this.environmentService.findAll();
|
||||
return { content: [{ type: 'text' as const, text: JSON.stringify(envs) }] };
|
||||
},
|
||||
);
|
||||
|
||||
server.registerTool(
|
||||
'get_environment',
|
||||
{
|
||||
description: 'Get an environment record by ID',
|
||||
inputSchema: {
|
||||
id: z.number().int().describe('Environment ID'),
|
||||
},
|
||||
},
|
||||
async ({ id }) => {
|
||||
try {
|
||||
const env = await this.environmentService.findOne(id);
|
||||
return { content: [{ type: 'text' as const, text: JSON.stringify(env) }] };
|
||||
} catch {
|
||||
return { isError: true, content: [{ type: 'text' as const, text: `Environment ${id} not found` }] };
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
server.registerTool(
|
||||
'create_environment',
|
||||
{
|
||||
description: 'Create a new named environment with a set of URLs',
|
||||
inputSchema: {
|
||||
name: z.string().describe('Unique environment name, e.g. liquio-diia-stg'),
|
||||
urls: z.record(z.string(), z.string()).describe('Map of URL keys to URL strings (id_url, cabinet_url, admin_url, …)'),
|
||||
},
|
||||
},
|
||||
async ({ name, urls }) => {
|
||||
try {
|
||||
const env = await this.environmentService.create({ name, urls: urls as EnvironmentUrls });
|
||||
return { content: [{ type: 'text' as const, text: JSON.stringify(env) }] };
|
||||
} catch (err) {
|
||||
return { isError: true, content: [{ type: 'text' as const, text: (err as Error).message }] };
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
server.registerTool(
|
||||
'update_environment',
|
||||
{
|
||||
description: 'Update an existing environment (name and/or urls)',
|
||||
inputSchema: {
|
||||
id: z.number().int().describe('Environment ID to update'),
|
||||
name: z.string().optional().describe('New name'),
|
||||
urls: z.record(z.string(), z.string()).optional().describe('New URLs map'),
|
||||
},
|
||||
},
|
||||
async ({ id, name, urls }) => {
|
||||
try {
|
||||
const env = await this.environmentService.update(id, { name, urls: urls as EnvironmentUrls | undefined });
|
||||
return { content: [{ type: 'text' as const, text: JSON.stringify(env) }] };
|
||||
} catch (err) {
|
||||
return { isError: true, content: [{ type: 'text' as const, text: (err as Error).message }] };
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
server.registerTool(
|
||||
'delete_environment',
|
||||
{
|
||||
description: 'Delete an environment by ID',
|
||||
inputSchema: {
|
||||
id: z.number().int().describe('Environment ID to delete'),
|
||||
},
|
||||
},
|
||||
async ({ id }) => {
|
||||
try {
|
||||
await this.environmentService.remove(id);
|
||||
return { content: [{ type: 'text' as const, text: `Environment ${id} deleted` }] };
|
||||
} catch (err) {
|
||||
return { isError: true, content: [{ type: 'text' as const, text: (err as Error).message }] };
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// ── Browser ───────────────────────────────────────────────────────────────
|
||||
|
||||
server.registerTool(
|
||||
'open_url',
|
||||
{
|
||||
description: 'Open a URL using a stored session and return the page title and content',
|
||||
inputSchema: {
|
||||
sessionName: z.string().describe('Session name to restore cookies and localStorage from'),
|
||||
url: z.string().url().describe('URL to navigate to'),
|
||||
readerMode: z.boolean().optional().describe('Extract readable plain text instead of raw HTML'),
|
||||
},
|
||||
},
|
||||
async ({ sessionName, url, readerMode }) => {
|
||||
try {
|
||||
const result = await this.browserService.open(sessionName, url, readerMode ?? false);
|
||||
return { content: [{ type: 'text' as const, text: JSON.stringify(result) }] };
|
||||
} catch (err) {
|
||||
return { isError: true, content: [{ type: 'text' as const, text: (err as Error).message }] };
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
server.registerTool(
|
||||
'exec_code',
|
||||
{
|
||||
description: 'Execute arbitrary Playwright JavaScript with `page` and `context` in scope',
|
||||
inputSchema: {
|
||||
sessionName: z.string().describe('Session name to restore'),
|
||||
code: z.string().describe('JavaScript code body to execute (async-safe, may use `page` and `context`)'),
|
||||
url: z.string().url().optional().describe('Optional URL to navigate to before running code'),
|
||||
},
|
||||
},
|
||||
async ({ sessionName, code, url }) => {
|
||||
try {
|
||||
const result = await this.browserService.exec(sessionName, code, url);
|
||||
return { content: [{ type: 'text' as const, text: JSON.stringify(result) }] };
|
||||
} catch (err) {
|
||||
return { isError: true, content: [{ type: 'text' as const, text: (err as Error).message }] };
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// ── Transport ─────────────────────────────────────────────────────────────
|
||||
|
||||
const transport = new StreamableHTTPServerTransport({
|
||||
sessionIdGenerator: undefined, // stateless — no session management
|
||||
});
|
||||
|
||||
await server.connect(transport);
|
||||
await transport.handleRequest(req, res, req.body);
|
||||
}
|
||||
}
|
||||
+2
-1
@@ -15,6 +15,7 @@
|
||||
"noImplicitAny": false,
|
||||
"strictBindCallApply": false,
|
||||
"forceConsistentCasingInFileNames": false,
|
||||
"noFallthroughCasesInSwitch": false
|
||||
"noFallthroughCasesInSwitch": false,
|
||||
"resolveJsonModule": true
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user