#1045: ✨ Complete MCP Server Scaffold#1065
Conversation
Implements a working MCP server for the Arranger introspection API, built on `@modelcontextprotocol/sdk` v1.x with the Streamable HTTP transport.
* Adds configuration via Zod-validated environment variables
* Adds Arranger connection validation at startup
* Adds Pino logging
* Adds an Express server with the configured MCP endpoint for Streamable HTTP transport
* Adds MCP Resources for the Arranger introspection endpoints:
* `arranger://introspection/server`
* `arranger://introspection/sqon`
* `arranger://introspection/catalog/{catalogId}`
* Adds MCP Tools for the Arranger introspection endpoints:
* `list-catalogs`
* `get-sqon-schema`
* `get-catalog-fields`
* Update `fieldShape` Zod schema in MCP tools to reflect new Arranger introspection response types * Update MCP Server validation unit tests to reflect new Arranger introspection response types * Update MCP Server integration tests to reflect new Arranger introspection response types
* Remove unnecessary `pnpm-lock.yaml`, this project exclusively uses `npm` not `pnpm`
* Moved `tsx` from `devDependencies` to `dependencies` in `apps/mcp-server/package.json` for the Jenkins Docker build (which runs `npm ci --omit=dev`)
* Updates `Dockerfile.local` and `Dockerfile.jenkins` (and their corresponding `.dockerignore` files) to include build targets for `mcp-server`
mistryrn
left a comment
There was a problem hiding this comment.
Leaving some comments calling out some of the decisions and TODOs. One other TODO for me is to give this PR a "catalogue"-pass (i.e. there are still catalog vs catalogue inconsistencies in some of the new code I've added, I need to clean that up)
| // This code was adapted from the official MCP Server "Streamable HTTP" example: | ||
| // https://github.com/modelcontextprotocol/typescript-sdk/blob/v1.x/src/examples/server/simpleStreamableHttp.ts | ||
| export const createHttpApp = (config: ArrangerMcpConfig, serverFactory: () => McpServer): McpHttpApp => { |
There was a problem hiding this comment.
As the comment states, this code is based off of the MCP "Streamable HTTP" example: https://github.com/modelcontextprotocol/typescript-sdk/blob/v1.x/src/examples/server/simpleStreamableHttp.ts
I haven't scrutinized it much myself yet as I've been focused on building the required functionality so far, but now that everything is up and running I'll take a closer look. Open to suggestions/improvements 🙏
| export const buildCatalogResources = ( | ||
| serverIntrospection: ArrangerServerIntrospection, | ||
| ): McpResourceDefinition[] => | ||
| Object.keys(serverIntrospection.catalogs).map((catalogId) => ({ | ||
| description: `Field-level introspection for the "${catalogId}" catalog.`, | ||
| name: `arranger_catalog_${catalogId}`, | ||
| uri: `arranger://introspection/catalog/${catalogId}`, | ||
| })); |
There was a problem hiding this comment.
Replaced the custom resource definitions in here with usages of the MCP SDK
| import { type McpServer } from '@modelcontextprotocol/sdk/server/mcp'; | ||
| import { z } from 'zod'; | ||
|
|
||
| export const buildFoundationTools = (): McpToolDefinition[] => [ |
There was a problem hiding this comment.
Similar to previous comment on resources.ts, replaced the custom tool definitions in here with usages of the MCP SDK
| const fieldShape = z.object({ | ||
| displayName: z.string(), | ||
| type: z.string(), | ||
| unit: z.string().nullable().optional(), | ||
| }); |
There was a problem hiding this comment.
Couldn't use the CatalogFieldIntrospection type exported from search-server here, as the MCP API wants Zod schemas (not types) for its inputSchema/outputSchema params.
Possible future enhancement/suggestion for the introspection types (https://github.com/overture-stack/arranger/blob/main/apps/search-server/src/introspection/types.ts#L38-L42):
- define as Zod schemas, then infer TS types from those schemas, giving us both options
e.g.,
export const CatalogFieldIntrospectionSchema = z.object({
displayName: z.string(),
type: z.string(),
unit: z.string().nullable().optional();
});
export type CatalogFieldIntrospection = z.infer<typeof CatalogFieldIntrospectionSchema>;There was a problem hiding this comment.
noted! adding a tech-debt item to be addressed soon
| // In-memory event store implementation from the MCP TypeScript SDK examples: | ||
| // https://github.com/modelcontextprotocol/typescript-sdk/blob/v1.x/src/examples/shared/inMemoryEventStore.ts | ||
| // TODO: Replace with a persistent storage solution for production use |
There was a problem hiding this comment.
Calling this out as well 🙏
There was a problem hiding this comment.
adding it to the tech-debt doc on my local (so that it's visible outside this file), to be committed later.
Severity: medium (state is lost on restart, no resumability for clients in production).
| | -------------------------- | ----------------------------------------------------------------------- | -------- | ------------ | ----------------------- | | ||
| | `ARRANGER_BASE_URL` | URL for the Arranger Server | `string` | **Required** | `http://localhost:5050` | | ||
| | `ARRANGER_CATALOGUES` | Comma-separated list of Arranger catalogues to expose to the MCP Server | `string` | **Required** | `server` | | ||
| | `ARRANGER_REQUEST_TIMEOUT` | Timeout for requests to Arranger | `number` | Optional | `10_0000` | |
There was a problem hiding this comment.
as defined in env.schema and code 👍
| | `ARRANGER_REQUEST_TIMEOUT` | Timeout for requests to Arranger | `number` | Optional | `10_0000` | | |
| | `ARRANGER_REQUEST_TIMEOUT_MS` | Timeout for requests to Arranger | `number` | Optional | `10_000` | |
| | `ARRANGER_REQUEST_TIMEOUT` | Timeout for requests to Arranger | `number` | Optional | `10_0000` | | ||
| | `MCP_HOST` | Host URL for the MCP server | `string` | Optional | `0.0.0.0` | | ||
| | `MCP_PORT` | Port the MCP Server will listen for requests on | `number` | Optional | `3100` | | ||
| | `MCP_PATH` | Endpoint for the MCP Stremable HTTP transport | `string` | Optional | `/mcp` | |
There was a problem hiding this comment.
tiny typo
| | `MCP_PATH` | Endpoint for the MCP Stremable HTTP transport | `string` | Optional | `/mcp` | | |
| | `MCP_PATH` | Endpoint for the MCP Streamable HTTP transport | `string` | Optional | `/mcp` | |
| logger.info(`MCP server running at http://${host}:${port}${path}`); | ||
| }); | ||
|
|
||
| process.on('SIGINT', async () => { |
There was a problem hiding this comment.
docker stop sends SIGTERM first; and containers that don't handle it will sit until the 10s SIGKILL timeout. SIGTERM should run the same graceful shutdown.
something along the lines of this is what needs to happen here
const shutdown = async () => {
logger.info('Shutting down server...');
await shutdownTransports();
logger.info('Server shutdown complete.');
process.exit(0);
};
process.on('SIGINT', shutdown);
process.on('SIGTERM', shutdown);| "@modelcontextprotocol/sdk": "^1.29.0", | ||
| "cors": "^2.8.6", | ||
| "dotenv": "^16.6.1", | ||
| "express": "^4.21.2", |
There was a problem hiding this comment.
we may need to bump this to ^5 across the repo, because of @mcp/sdk's internal express dep version... importing the types for 4 could lead to maintenance complications wherever the API changes drift apart
TODO?
| "prettier": "^3.4.2", | ||
| "tsx": "^4.19.3" | ||
| }, | ||
| "private": "true", |
There was a problem hiding this comment.
while npm takes both/either, switching to bool for consistency with all other modules/packages in this repo
| "private": "true", | |
| "private": true, |
| const structured = { catalogId: data.catalogId, fields: data.fields }; | ||
| return { | ||
| content: [{ type: 'text', text: JSON.stringify(structured, null, 2) }], | ||
| structuredContent: structured, |
There was a problem hiding this comment.
not wrong, strictly speaking, but worth noting for future implementation:
structuredContent here makes this tool machine-readable for MCP clients... the other two tools return plain text; this inconsistency will matter when clients start consuming the these programmatically rather than via MCP Inspector
| if (sid && transports[sid]) { | ||
| logger.info(`Transport closed for session ${sid}, removing from transports map`); | ||
| // eslint-disable-next-line @typescript-eslint/no-dynamic-delete | ||
| delete transports[sid]; |
There was a problem hiding this comment.
while I see you nicely cleaning transports on delete and shutdown, the entry will persist if the connection is closed poorly (e.g. drop, crash).
to prevent PR scope creep, I'm adding a relevant item in the tech-debt doc, to do timestap tracking and expiration.
e.g.
const transports: Record<string, { transport: StreamableHTTPServerTransport; lastSeenAt: number }> = {};
// update lastSeenAt on every request that hits an existing session
// setInterval every 5 min, evict anything idle > 30 min| @@ -1,27 +1,58 @@ | |||
| import type { McpToolDefinition } from './types.js'; | |||
| import { type McpServer } from '@modelcontextprotocol/sdk/server/mcp'; | |||
| import { z } from 'zod'; | |||
There was a problem hiding this comment.
let's try to keep consistent use of z as zod imports, to avoid single letter variable names, please
879e561 to
6de99b3
Compare
Summary
Implements a working MCP server for the Arranger introspection API, built on
@modelcontextprotocol/sdkv1.x with the Streamable HTTP transport. Adds MCP Resources and Tools for the Arranger introspection endpoints, and supporting unit tests and integration tests. Also adds Docker build targets for the MCP Server to both the local and Jenkins Dockerfiles.Issues
Description of Changes
MCP Server
@modelcontextprotocol/sdk,zod(v3 for consistency with repo), andpinoas dependenciesarranger-server-introspection→arranger://introspection/serverarranger-sqon-schema→arranger://introspection/sqonarranger-catalog-fields→arranger://introspection/catalog/{catalogId}list-catalogsget-sqon-schemaget-catalog-fieldsSpecial Instructions
Before running these changes, you will need to:
# from project root npm ci# from project root npm run modules:buildmcp-serverapp:Testing Instructions
Local Arranger
To test the MCP Server against a local instance of Arranger Server:
apps/mcp-server/.envconfig aligns with your local Arranger server# from project root npm run mcp-server:dev# from project root npm run mcp-server:inspecthttp://localhost:6274/?MCP_PROXY_AUTH_TOKEN={AUTH_TOKEN}), connect to the MCP Server via Streamable HTTP, and test the Resources and Tools.Remote Arranger
To test against a remote instance of Arranger Server:
ARRANGER_BASE_URLandARRANGER_CATALOGUESin your MCP Server.envfile to point to and reflect the state of your remote Arranger.LM Studio
To test with LM Studio instead of MCP Inspector:
apps/mcp-server/mcp-inspector.jsonDocker
To test local Docker builds:
# from project root docker run --env-file apps/mcp-server/.env -p 3100:3100 arranger/mcp-server:localThe server will be running at
http://localhost:3100and you can test it with MCP Inspector or LM Studio as per the previous instructions.Note
To test the Docker image with a local Arranger Server, update your
.envto usehost.docker.internalinstead oflocalhostas part of theARRANGER_BASE_URL.Readiness Checklist
.env.schemafile and documented in the README