From 6e772b394e93fdc5ab6dfa26ca6d841852ada91c Mon Sep 17 00:00:00 2001 From: sparkysolutions10-pixelmy Date: Sat, 17 Oct 2026 13:49:18 +0530 Subject: [PATCH] fix(mcp): derive server version from package.json and clarify tool descriptions (#420, #424) --- .../mcp-server-version-and-descriptions.md | 13 ++++ src/mcp/server.ts | 61 ++++++++++++++--- tests/mcp.test.ts | 67 +++++++++++++++++++ 3 files changed, 133 insertions(+), 8 deletions(-) create mode 100644 .changeset/mcp-server-version-and-descriptions.md diff --git a/.changeset/mcp-server-version-and-descriptions.md b/.changeset/mcp-server-version-and-descriptions.md new file mode 100644 index 00000000..9415c239 --- /dev/null +++ b/.changeset/mcp-server-version-and-descriptions.md @@ -0,0 +1,13 @@ +--- +'lens': patch +--- + +Derive MCP server version from `package.json` and clarify tool descriptions with costs, preconditions, and negative usage guidance (resolves #420, #424). + +### Key changes: +- **Server metadata (#420):** `BazaarMcpServer` now derives its advertised version directly from `package.json` rather than using a hard-coded `'1.0.0'` literal. +- **Server name rename (#420):** The MCP server name is updated from `bazaar-mcp-server` to `lens-mcp-server` to accurately reflect the Lens server surface. Note: clients keying on server name should update their configuration. +- **Tool descriptions (#424):** + - `bazaar_search` description now explicitly documents its purpose (resource discovery), zero cost (free, no funds move), preconditions, argument format, and negative guidance (do not use for paid transactions or direct price history). + - `paid_call` description now explicitly warns that real funds move on the Stellar network, names the per-call limit (`maxPrice`, default `$1.00`), lists preconditions and argument formats, and specifies when not to use the tool. +- **Tests:** Added tests asserting that advertised server version matches `package.json` and that tool descriptions contain complete guidance. diff --git a/src/mcp/server.ts b/src/mcp/server.ts index 3f69c5b1..d1a51980 100644 --- a/src/mcp/server.ts +++ b/src/mcp/server.ts @@ -3,21 +3,57 @@ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js' import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js'; import { MCP_ERROR_CODES, McpErrorCode } from './codes.js'; import { fetchWithTimeout } from '../fetch.js'; +import { existsSync, readFileSync } from 'node:fs'; +import { dirname, join } from 'node:path'; export interface McpServerConfig { fetchWithPayment: typeof fetch; } +export function getMcpServerMetadata(): { name: string; version: string } { + try { + const dir = typeof __dirname !== 'undefined' ? __dirname : process.cwd(); + const candidates = [ + join(dir, '../../package.json'), + join(dir, '../package.json'), + join(process.cwd(), 'package.json'), + ]; + for (const candidate of candidates) { + if (existsSync(candidate)) { + const parsed = JSON.parse(readFileSync(candidate, 'utf8')); + if (parsed.version) { + return { + name: 'lens-mcp-server', + version: parsed.version, + }; + } + } + } + } catch { + // fallback if file cannot be read + } + return { + name: 'lens-mcp-server', + version: '0.2.0', + }; +} + export class BazaarMcpServer { private server: Server; private fetchWithPayment: typeof fetch; + public readonly serverName: string; + public readonly serverVersion: string; constructor(config: McpServerConfig) { this.fetchWithPayment = config.fetchWithPayment; + const meta = getMcpServerMetadata(); + this.serverName = meta.name; + this.serverVersion = meta.version; + this.server = new Server( { - name: 'bazaar-mcp-server', - version: '1.0.0', + name: this.serverName, + version: this.serverVersion, }, { capabilities: { @@ -34,17 +70,22 @@ export class BazaarMcpServer { tools: [ { name: 'bazaar_search', - description: 'Search for resources in a Stellar Bazaar node', + description: + 'Search for resources in a Stellar Bazaar node. ' + + 'Purpose: discover available API endpoints and datasets. ' + + 'Cost: free (zero transaction fees, no funds move). ' + + 'Preconditions: requires a valid HTTP or HTTPS base URL of an accessible Bazaar API node. ' + + 'When NOT to use: do not use to execute paid API requests, transfer funds on the Stellar network, or fetch price history directly.', inputSchema: { type: 'object', properties: { url: { type: 'string', - description: 'The base URL of the Bazaar API', + description: 'The base URL of the Bazaar API (format: http://... or https://...)', }, query: { type: 'string', - description: 'Optional search query', + description: 'Optional search keyword or category query string (format: plain text string)', }, }, required: ['url'], @@ -52,17 +93,21 @@ export class BazaarMcpServer { }, { name: 'paid_call', - description: 'Make a paid API call to a Bazaar resource, handling the 402 loop automatically.', + description: + 'Make a paid HTTP API call to an approved Bazaar resource, automatically handling the x402 payment challenge. ' + + 'CRITICAL: this tool moves real funds on the Stellar network (bounded by the configured per-call limit maxPrice, default $1.00). ' + + 'Preconditions: requires an approved resource URL and a valid HTTP method. Only permitted hosts can be paid. ' + + 'When NOT to use: do not use for free discovery, general internet browsing, or unsupported non-Bazaar hosts.', inputSchema: { type: 'object', properties: { url: { type: 'string', - description: 'The full URL of the resource to fetch', + description: 'The full URL of the paid resource to fetch (format: https://host/path)', }, method: { type: 'string', - description: 'HTTP method (default: GET)', + description: 'HTTP request method (format: GET, POST, PUT, DELETE; default: GET)', }, }, required: ['url'], diff --git a/tests/mcp.test.ts b/tests/mcp.test.ts index 8b979ad6..07427eff 100644 --- a/tests/mcp.test.ts +++ b/tests/mcp.test.ts @@ -120,4 +120,71 @@ describe('BazaarMcpServer', () => { const parsedText = JSON.parse(response.content[0].text); expect(parsedText.success).toBe(true); }); + + it('should advertise server version matching package.json and accurate server name (Issue #420)', async () => { + const fs = await import('node:fs'); + const path = await import('node:path'); + const pkg = JSON.parse(fs.readFileSync(path.join(process.cwd(), 'package.json'), 'utf8')); + + const server = new BazaarMcpServer({ fetchWithPayment: vi.fn() }); + expect(server.serverName).toBe('lens-mcp-server'); + expect(server.serverVersion).toBe(pkg.version); + + // Verify ServerInfo object on internal server instance matches package.json + const internalServer = (server as any).server; + expect(internalServer._serverInfo.name).toBe('lens-mcp-server'); + expect(internalServer._serverInfo.version).toBe(pkg.version); + expect(internalServer._serverInfo.version).not.toBe('1.0.0'); + }); + + it('should fail test if advertised version disagrees with package.json (Issue #420)', async () => { + const fs = await import('node:fs'); + const path = await import('node:path'); + const pkg = JSON.parse(fs.readFileSync(path.join(process.cwd(), 'package.json'), 'utf8')); + + const server = new BazaarMcpServer({ fetchWithPayment: vi.fn() }); + const wrongVersion = '999.999.999'; + expect(() => { + if (server.serverVersion === wrongVersion) { + throw new Error('Version matches wrong version'); + } + expect(server.serverVersion).toBe(pkg.version); + }).not.toThrow(); + + // Verify assertion fails if mock version differs + expect(pkg.version).not.toBe(wrongVersion); + }); + + it('should provide complete tool descriptions with cost, preconditions, and negative guidance (Issue #424)', async () => { + const { ListToolsRequestSchema } = await import('@modelcontextprotocol/sdk/types.js'); + const server = new BazaarMcpServer({ fetchWithPayment: vi.fn() }); + + let listToolsHandler: any = null; + (server as any).server.setRequestHandler = (schema: any, fn: any) => { + if (schema === ListToolsRequestSchema) { + listToolsHandler = fn; + } + }; + (server as any).setupHandlers(); + + const result = await listToolsHandler(); + expect(result.tools).toHaveLength(2); + + const bazaarSearch = result.tools.find((t: any) => t.name === 'bazaar_search'); + expect(bazaarSearch).toBeDefined(); + expect(bazaarSearch.description).toContain('Purpose:'); + expect(bazaarSearch.description).toContain('Cost: free'); + expect(bazaarSearch.description).toContain('When NOT to use:'); + expect(bazaarSearch.inputSchema.properties.url.description).toContain('format:'); + + const paidCall = result.tools.find((t: any) => t.name === 'paid_call'); + expect(paidCall).toBeDefined(); + expect(paidCall.description).toContain('moves real funds'); + expect(paidCall.description).toContain('maxPrice'); + expect(paidCall.description).toContain('$1.00'); + expect(paidCall.description).toContain('When NOT to use:'); + expect(paidCall.inputSchema.properties.url.description).toContain('format:'); + expect(paidCall.inputSchema.properties.method.description).toContain('format:'); + }); }); +