feat(mcp): add MCP Server Integration — Property Search, Analytics, Valuation
Implement 3 MCP servers in libs/mcp-servers/ using @modelcontextprotocol/sdk: - Property Search: NL search via Typesense, property comparison, detail lookup - Market Analytics: market reports, price trends, district comparison - Valuation: AVM integration with Python AI service, feature extraction, batch valuation Includes NestJS integration module with SSE transport for in-process hosting. Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
63
libs/mcp-servers/src/nestjs/mcp-registry.service.ts
Normal file
63
libs/mcp-servers/src/nestjs/mcp-registry.service.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import { Injectable, Inject, type OnModuleInit } from '@nestjs/common';
|
||||
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
||||
import { MCP_MODULE_OPTIONS, type McpModuleOptions } from './mcp.module';
|
||||
|
||||
@Injectable()
|
||||
export class McpRegistryService implements OnModuleInit {
|
||||
private readonly servers = new Map<string, McpServer>();
|
||||
private typesenseClient: import('typesense').Client | null = null;
|
||||
|
||||
constructor(
|
||||
@Inject(MCP_MODULE_OPTIONS) private readonly options: McpModuleOptions,
|
||||
) {}
|
||||
|
||||
async onModuleInit(): Promise<void> {
|
||||
// Lazy import to avoid hard dependency at module load time
|
||||
const { createPropertySearchServer } = await import('../property-search/property-search.server');
|
||||
const { createMarketAnalyticsServer } = await import('../market-analytics/market-analytics.server');
|
||||
const { createValuationServer } = await import('../valuation/valuation.server');
|
||||
|
||||
// Typesense client is injected from the host app via setTypesenseClient
|
||||
// If not set by the time servers are needed, tools that require it will fail gracefully
|
||||
if (this.typesenseClient) {
|
||||
this.servers.set(
|
||||
'property-search',
|
||||
createPropertySearchServer({
|
||||
typesenseClient: this.typesenseClient,
|
||||
collectionName: this.options.typesenseCollectionName,
|
||||
}),
|
||||
);
|
||||
|
||||
this.servers.set(
|
||||
'market-analytics',
|
||||
createMarketAnalyticsServer({
|
||||
typesenseClient: this.typesenseClient,
|
||||
collectionName: this.options.typesenseCollectionName,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
this.servers.set(
|
||||
'valuation',
|
||||
createValuationServer({
|
||||
aiServiceBaseUrl: this.options.aiServiceBaseUrl,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
setTypesenseClient(client: import('typesense').Client): void {
|
||||
this.typesenseClient = client;
|
||||
}
|
||||
|
||||
getServer(name: string): McpServer | undefined {
|
||||
return this.servers.get(name);
|
||||
}
|
||||
|
||||
getServerNames(): string[] {
|
||||
return Array.from(this.servers.keys());
|
||||
}
|
||||
|
||||
getAllServers(): Map<string, McpServer> {
|
||||
return this.servers;
|
||||
}
|
||||
}
|
||||
56
libs/mcp-servers/src/nestjs/mcp-transport.controller.ts
Normal file
56
libs/mcp-servers/src/nestjs/mcp-transport.controller.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import { Controller, Get, Post, Param, Req, Res, HttpException, HttpStatus } from '@nestjs/common';
|
||||
import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';
|
||||
import type { Request, Response } from 'express';
|
||||
import { McpRegistryService } from './mcp-registry.service';
|
||||
|
||||
@Controller('mcp')
|
||||
export class McpTransportController {
|
||||
private readonly transports = new Map<string, SSEServerTransport>();
|
||||
|
||||
constructor(private readonly registry: McpRegistryService) {}
|
||||
|
||||
@Get('servers')
|
||||
listServers(): { servers: string[] } {
|
||||
return { servers: this.registry.getServerNames() };
|
||||
}
|
||||
|
||||
@Get(':serverName/sse')
|
||||
async handleSse(
|
||||
@Param('serverName') serverName: string,
|
||||
@Req() req: Request,
|
||||
@Res() res: Response,
|
||||
): Promise<void> {
|
||||
const server = this.registry.getServer(serverName);
|
||||
if (!server) {
|
||||
throw new HttpException(`MCP server "${serverName}" not found`, HttpStatus.NOT_FOUND);
|
||||
}
|
||||
|
||||
const transport = new SSEServerTransport(`/mcp/${serverName}/messages`, res);
|
||||
this.transports.set(transport.sessionId, transport);
|
||||
|
||||
req.on('close', () => {
|
||||
this.transports.delete(transport.sessionId);
|
||||
});
|
||||
|
||||
await server.connect(transport);
|
||||
}
|
||||
|
||||
@Post(':serverName/messages')
|
||||
async handleMessage(
|
||||
@Param('serverName') serverName: string,
|
||||
@Req() req: Request,
|
||||
@Res() res: Response,
|
||||
): Promise<void> {
|
||||
const sessionId = req.query['sessionId'] as string;
|
||||
if (!sessionId) {
|
||||
throw new HttpException('Missing sessionId query parameter', HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
|
||||
const transport = this.transports.get(sessionId);
|
||||
if (!transport) {
|
||||
throw new HttpException('Session not found or expired', HttpStatus.NOT_FOUND);
|
||||
}
|
||||
|
||||
await transport.handlePostMessage(req, res);
|
||||
}
|
||||
}
|
||||
28
libs/mcp-servers/src/nestjs/mcp.module.ts
Normal file
28
libs/mcp-servers/src/nestjs/mcp.module.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { Module, type DynamicModule, type Provider } from '@nestjs/common';
|
||||
import { McpRegistryService } from './mcp-registry.service';
|
||||
import { McpTransportController } from './mcp-transport.controller';
|
||||
|
||||
export interface McpModuleOptions {
|
||||
aiServiceBaseUrl: string;
|
||||
typesenseCollectionName?: string;
|
||||
}
|
||||
|
||||
export const MCP_MODULE_OPTIONS = Symbol('MCP_MODULE_OPTIONS');
|
||||
|
||||
@Module({})
|
||||
export class McpModule {
|
||||
static forRoot(options: McpModuleOptions): DynamicModule {
|
||||
const optionsProvider: Provider = {
|
||||
provide: MCP_MODULE_OPTIONS,
|
||||
useValue: options,
|
||||
};
|
||||
|
||||
return {
|
||||
module: McpModule,
|
||||
controllers: [McpTransportController],
|
||||
providers: [optionsProvider, McpRegistryService],
|
||||
exports: [McpRegistryService],
|
||||
global: false,
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user