diff --git a/CHANGELOG.md b/CHANGELOG.md index 2fc036d..773529f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ ## Unreleased +### Removed + +- **version_tool**: Removed from the tool list — version info is now available as a resource at `mapbox://version` with zero token overhead + +### New Features + +- **mapbox://version resource**: Server version, git SHA, tag, and branch accessible via `readResource('mapbox://version')` + ## 0.10.0 - 2026-03-04 ### New Features diff --git a/src/resources/index.ts b/src/resources/index.ts index 60ac245..e6a897a 100644 --- a/src/resources/index.ts +++ b/src/resources/index.ts @@ -30,13 +30,17 @@ import { httpRequest } from '../utils/httpPipeline.js'; // Export all resource classes export { CategoryListResource } from './category-list/CategoryListResource.js'; +export { VersionResource } from './version/VersionResource.js'; // Import resource classes for instantiation import { CategoryListResource } from './category-list/CategoryListResource.js'; +import { VersionResource } from './version/VersionResource.js'; // Export pre-configured resource instances with short, clean names /** Category list for place search (mapbox://categories) */ export const categoryList = new CategoryListResource({ httpRequest }); +/** Server version info (mapbox://version) */ +export const version = new VersionResource(); // Export registry functions for batch access export { diff --git a/src/resources/resourceRegistry.ts b/src/resources/resourceRegistry.ts index 0b9d59a..8d0c0ed 100644 --- a/src/resources/resourceRegistry.ts +++ b/src/resources/resourceRegistry.ts @@ -5,6 +5,7 @@ import { CategoryListResource } from './category-list/CategoryListResource.js'; import { TemporaryDataResource } from './temporary/TemporaryDataResource.js'; import { StaticMapUIResource } from './ui-apps/StaticMapUIResource.js'; +import { VersionResource } from './version/VersionResource.js'; import { httpRequest } from '../utils/httpPipeline.js'; // Central registry of all resources @@ -12,7 +13,8 @@ export const ALL_RESOURCES = [ // INSERT NEW RESOURCE INSTANCE HERE new CategoryListResource({ httpRequest }), new TemporaryDataResource(), - new StaticMapUIResource() + new StaticMapUIResource(), + new VersionResource() ] as const; export type ResourceInstance = (typeof ALL_RESOURCES)[number]; diff --git a/src/resources/version/VersionResource.ts b/src/resources/version/VersionResource.ts new file mode 100644 index 0000000..d7645de --- /dev/null +++ b/src/resources/version/VersionResource.ts @@ -0,0 +1,33 @@ +// Copyright (c) Mapbox, Inc. +// Licensed under the MIT License. + +import { BaseResource } from '../BaseResource.js'; +import { getVersionInfo } from '../../utils/versionUtils.js'; +import type { ReadResourceResult } from '@modelcontextprotocol/sdk/types.js'; + +/** + * Resource exposing MCP server version information. + * + * Available URI: + * - mapbox://version + */ +export class VersionResource extends BaseResource { + readonly uri = 'mapbox://version'; + readonly name = 'Mapbox MCP Server Version'; + readonly description = + 'Version information for the Mapbox MCP server, including version number, git SHA, tag, and branch.'; + readonly mimeType = 'application/json'; + + async read(): Promise { + const info = getVersionInfo(); + return { + contents: [ + { + uri: this.uri, + mimeType: 'application/json', + text: JSON.stringify(info, null, 2) + } + ] + }; + } +} diff --git a/src/tools/index.ts b/src/tools/index.ts index 83d01a3..0ffe034 100644 --- a/src/tools/index.ts +++ b/src/tools/index.ts @@ -49,8 +49,6 @@ export { ReverseGeocodeTool } from './reverse-geocode-tool/ReverseGeocodeTool.js export { SearchAndGeocodeTool } from './search-and-geocode-tool/SearchAndGeocodeTool.js'; export { SimplifyTool } from './simplify-tool/SimplifyTool.js'; export { StaticMapImageTool } from './static-map-image-tool/StaticMapImageTool.js'; -export { VersionTool } from './version-tool/VersionTool.js'; - // Import tool classes for instantiation import { AreaTool } from './area-tool/AreaTool.js'; import { BearingTool } from './bearing-tool/BearingTool.js'; @@ -72,8 +70,6 @@ import { ReverseGeocodeTool } from './reverse-geocode-tool/ReverseGeocodeTool.js import { SearchAndGeocodeTool } from './search-and-geocode-tool/SearchAndGeocodeTool.js'; import { SimplifyTool } from './simplify-tool/SimplifyTool.js'; import { StaticMapImageTool } from './static-map-image-tool/StaticMapImageTool.js'; -import { VersionTool } from './version-tool/VersionTool.js'; - // Export pre-configured tool instances with short, clean names // Note: Import path already indicates these are tools, so we omit the "Tool" suffix @@ -137,9 +133,6 @@ export const simplify = new SimplifyTool(); /** Generate static map images */ export const staticMapImage = new StaticMapImageTool({ httpRequest }); -/** Get version information */ -export const version = new VersionTool(); - // Export registry functions for batch access export { getCoreTools, diff --git a/src/tools/toolRegistry.ts b/src/tools/toolRegistry.ts index 614bd2b..b577d3b 100644 --- a/src/tools/toolRegistry.ts +++ b/src/tools/toolRegistry.ts @@ -23,7 +23,6 @@ import { ResourceReaderTool } from './resource-reader-tool/ResourceReaderTool.js import { ReverseGeocodeTool } from './reverse-geocode-tool/ReverseGeocodeTool.js'; import { StaticMapImageTool } from './static-map-image-tool/StaticMapImageTool.js'; import { SearchAndGeocodeTool } from './search-and-geocode-tool/SearchAndGeocodeTool.js'; -import { VersionTool } from './version-tool/VersionTool.js'; import { httpRequest } from '../utils/httpPipeline.js'; /** @@ -42,7 +41,6 @@ export const CORE_TOOLS = [ new BufferTool(), new PointInPolygonTool(), new DistanceTool(), - new VersionTool(), new CategorySearchTool({ httpRequest }), new DirectionsTool({ httpRequest }), new IsochroneTool({ httpRequest }), diff --git a/src/tools/version-tool/VersionTool.input.schema.ts b/src/tools/version-tool/VersionTool.input.schema.ts deleted file mode 100644 index baa775c..0000000 --- a/src/tools/version-tool/VersionTool.input.schema.ts +++ /dev/null @@ -1,6 +0,0 @@ -// Copyright (c) Mapbox, Inc. -// Licensed under the MIT License. - -import { z } from 'zod'; - -export const VersionSchema = z.object({}); diff --git a/src/tools/version-tool/VersionTool.output.schema.ts b/src/tools/version-tool/VersionTool.output.schema.ts deleted file mode 100644 index ed2c377..0000000 --- a/src/tools/version-tool/VersionTool.output.schema.ts +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright (c) Mapbox, Inc. -// Licensed under the MIT License. - -import { z } from 'zod'; - -// Schema for version tool output - matches the VersionInfo interface -export const VersionResponseSchema = z.object({ - name: z.string(), - version: z.string(), - sha: z.string(), - tag: z.string(), - branch: z.string() -}); - -export type VersionResponse = z.infer; diff --git a/src/tools/version-tool/VersionTool.ts b/src/tools/version-tool/VersionTool.ts deleted file mode 100644 index 544a7c0..0000000 --- a/src/tools/version-tool/VersionTool.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { context, SpanStatusCode, trace } from '@opentelemetry/api'; -import { createLocalToolExecutionContext } from '../../utils/tracing.js'; -// Copyright (c) Mapbox, Inc. -// Licensed under the MIT License. - -import { BaseTool } from '../BaseTool.js'; -import { getVersionInfo } from '../../utils/versionUtils.js'; -import { VersionSchema } from './VersionTool.input.schema.js'; -import { VersionResponseSchema } from './VersionTool.output.schema.js'; -import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js'; - -export class VersionTool extends BaseTool< - typeof VersionSchema, - typeof VersionResponseSchema -> { - readonly name = 'version_tool'; - readonly description = - 'Get the current version information of the MCP server'; - readonly annotations = { - title: 'Version Information Tool', - readOnlyHint: true, - destructiveHint: false, - idempotentHint: true, - openWorldHint: false - }; - - constructor() { - super({ - inputSchema: VersionSchema, - outputSchema: VersionResponseSchema - }); - } - - // eslint-disable-next-line @typescript-eslint/no-unused-vars - async run(_rawInput: unknown): Promise { - // Create tracing context for this tool - const toolContext = createLocalToolExecutionContext(this.name, 0); - return await context.with( - trace.setSpan(context.active(), toolContext.span), - async () => { - try { - const versionInfo = getVersionInfo(); - const versionText = `MCP Server Version Information:\n- Name: ${versionInfo.name}\n- Version: ${versionInfo.version}\n- SHA: ${versionInfo.sha}\n- Tag: ${versionInfo.tag}\n- Branch: ${versionInfo.branch}`; - - // Validate with graceful fallback - const validatedVersionInfo = this.validateOutput( - versionInfo - ) as Record; - - toolContext.span.setStatus({ code: SpanStatusCode.OK }); - toolContext.span.end(); - return { - content: [{ type: 'text' as const, text: versionText }], - structuredContent: validatedVersionInfo, - isError: false - }; - } catch (error) { - const errorMessage = - error instanceof Error ? error.message : String(error); - toolContext.span.setStatus({ - code: SpanStatusCode.ERROR, - message: errorMessage - }); - toolContext.span.end(); - this.log( - 'error', - `${this.name}: Error during execution: ${errorMessage}` - ); - return { - content: [ - { - type: 'text' as const, - text: `VersionTool: Error during execution: ${errorMessage}` - } - ], - isError: true - }; - } - } - ); - } -} diff --git a/test/tools/version-tool/VersionTool.output.schema.test.ts b/test/tools/version-tool/VersionTool.output.schema.test.ts deleted file mode 100644 index f51d0e9..0000000 --- a/test/tools/version-tool/VersionTool.output.schema.test.ts +++ /dev/null @@ -1,161 +0,0 @@ -// Copyright (c) Mapbox, Inc. -// Licensed under the MIT License. - -process.env.MAPBOX_ACCESS_TOKEN = - 'eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ0ZXN0In0.signature'; - -import { describe, it, expect, vi } from 'vitest'; -import { VersionTool } from '../../../src/tools/version-tool/VersionTool.js'; - -describe('VersionTool output schema registration', () => { - it('should have an output schema defined', () => { - const tool = new VersionTool(); - expect(tool.outputSchema).toBeDefined(); - expect(tool.outputSchema).toBeTruthy(); - }); - - it('should register output schema with MCP server', () => { - const tool = new VersionTool(); - - // Mock the installTo method to verify it gets called with output schema - const mockInstallTo = vi.fn().mockImplementation(() => { - // Verify that the tool has an output schema when being installed - expect(tool.outputSchema).toBeDefined(); - return tool; - }); - - Object.defineProperty(tool, 'installTo', { - value: mockInstallTo - }); - - // Simulate server registration - tool.installTo({} as never); - expect(mockInstallTo).toHaveBeenCalled(); - }); - - it('should validate valid version response structure', () => { - const validResponse = { - name: 'Mapbox MCP server', - version: '0.5.5', - sha: 'a64ffb6e4b4017c0f9ae7259be53bb372301fea5', - tag: 'v0.5.5-1-ga64ffb6', - branch: 'structured_content_public' - }; - - const tool = new VersionTool(); - - // This should not throw if the schema is correct - expect(() => { - if (tool.outputSchema) { - tool.outputSchema.parse(validResponse); - } - }).not.toThrow(); - }); - - it('should validate minimal version response with unknown values', () => { - const minimalResponse = { - name: 'Mapbox MCP server', - version: '0.0.0', - sha: 'unknown', - tag: 'unknown', - branch: 'unknown' - }; - - const tool = new VersionTool(); - - expect(() => { - if (tool.outputSchema) { - tool.outputSchema.parse(minimalResponse); - } - }).not.toThrow(); - }); - - it('should validate development version response', () => { - const devResponse = { - name: 'Mapbox MCP server', - version: '1.0.0-dev', - sha: 'abc123def456', - tag: 'dev-build', - branch: 'feature/new-feature' - }; - - const tool = new VersionTool(); - - expect(() => { - if (tool.outputSchema) { - tool.outputSchema.parse(devResponse); - } - }).not.toThrow(); - }); - - it('should throw validation error for missing required fields', () => { - const invalidResponse = { - name: 'Mapbox MCP server', - version: '0.5.5' - // Missing sha, tag, branch fields - }; - - const tool = new VersionTool(); - - expect(() => { - if (tool.outputSchema) { - tool.outputSchema.parse(invalidResponse); - } - }).toThrow(); - }); - - it('should throw validation error for wrong field types', () => { - const invalidTypeResponse = { - name: 'Mapbox MCP server', - version: 1.0, // Should be string, not number - sha: 'abc123', - tag: 'v1.0.0', - branch: 'main' - }; - - const tool = new VersionTool(); - - expect(() => { - if (tool.outputSchema) { - tool.outputSchema.parse(invalidTypeResponse); - } - }).toThrow(); - }); - - it('should throw validation error for empty string fields', () => { - const emptyFieldResponse = { - name: '', // Empty string - version: '0.5.5', - sha: 'abc123', - tag: 'v0.5.5', - branch: 'main' - }; - - const tool = new VersionTool(); - - // All fields are required and should be non-empty strings - expect(() => { - if (tool.outputSchema) { - tool.outputSchema.parse(emptyFieldResponse); - } - }).not.toThrow(); // Actually, empty strings are valid strings in Zod - }); - - it('should throw validation error when fields are null', () => { - const nullFieldResponse = { - name: 'Mapbox MCP server', - version: null, // Should be string, not null - sha: 'abc123', - tag: 'v0.5.5', - branch: 'main' - }; - - const tool = new VersionTool(); - - expect(() => { - if (tool.outputSchema) { - tool.outputSchema.parse(nullFieldResponse); - } - }).toThrow(); - }); -}); diff --git a/test/tools/version-tool/VersionTool.test.ts b/test/tools/version-tool/VersionTool.test.ts deleted file mode 100644 index 45cf689..0000000 --- a/test/tools/version-tool/VersionTool.test.ts +++ /dev/null @@ -1,102 +0,0 @@ -// Copyright (c) Mapbox, Inc. -// Licensed under the MIT License. - -import { describe, it, expect, beforeEach, vi } from 'vitest'; -import type { MockedFunction } from 'vitest'; -import { getVersionInfo } from '../../../src/utils/versionUtils.js'; -import { VersionTool } from '../../../src/tools/version-tool/VersionTool.js'; - -vi.mock('../../../src/utils/versionUtils.js', () => ({ - getVersionInfo: vi.fn(() => ({ - name: 'Test MCP Server', - version: '1.0.0', - sha: 'abc123', - tag: 'v1.0.0', - branch: 'main' - })) -})); - -const mockGetVersionInfo = getVersionInfo as MockedFunction< - typeof getVersionInfo ->; - -describe('VersionTool', () => { - let tool: VersionTool; - - beforeEach(() => { - tool = new VersionTool(); - }); - - describe('run', () => { - it('should return version information', async () => { - const result = await tool.run({}); - - expect(result.isError).toBe(false); - expect(result.content).toHaveLength(1); - - // Best approach: exact match with template literal for readability and precision - const expectedText = `MCP Server Version Information: -- Name: Test MCP Server -- Version: 1.0.0 -- SHA: abc123 -- Tag: v1.0.0 -- Branch: main`; - expect(result.content[0]).toEqual({ - type: 'text', - text: expectedText - }); - - // Verify structured content is included - expect(result.structuredContent).toBeDefined(); - expect(result.structuredContent).toEqual({ - name: 'Test MCP Server', - version: '1.0.0', - sha: 'abc123', - tag: 'v1.0.0', - branch: 'main' - }); - }); - - it('should handle fallback version info correctly', async () => { - // Mock getVersionInfo to return fallback values (which is realistic behavior) - mockGetVersionInfo.mockImplementationOnce(() => ({ - name: 'Mapbox MCP server', - version: '0.0.0', - sha: 'unknown', - tag: 'unknown', - branch: 'unknown' - })); - - const result = await tool.run({}); - - expect(result.isError).toBe(false); - expect(result.content).toHaveLength(1); - expect(result.content[0].type).toBe('text'); - expect( - (result.content[0] as { type: 'text'; text: string }).text - ).toContain('Version: 0.0.0'); - expect( - (result.content[0] as { type: 'text'; text: string }).text - ).toContain('SHA: unknown'); - expect(result.structuredContent).toEqual({ - name: 'Mapbox MCP server', - version: '0.0.0', - sha: 'unknown', - tag: 'unknown', - branch: 'unknown' - }); - }); - }); - - describe('properties', () => { - it('should have correct name', () => { - expect(tool.name).toBe('version_tool'); - }); - - it('should have correct description', () => { - expect(tool.description).toBe( - 'Get the current version information of the MCP server' - ); - }); - }); -});