Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions cspell.config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"version": "0.2",
"language": "en",
"words": [
"bbox",
"denoise",
"isochrone",
"mapbox",
"mmss",
"tilequery"
],
"ignorePaths": [
"node_modules",
"dist"
]
}
1,037 changes: 1,034 additions & 3 deletions package-lock.json

Large diffs are not rendered by default.

15 changes: 9 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,18 @@
"mcp-server": "dist/esm/index.js"
},
"scripts": {
"lint": "eslint \"./src/**/*.{ts,tsx}\" \"./test/**/*.{ts,tsx}\"",
"lint:fix": "eslint \"./src/**/*.{ts,tsx}\" \"./test/**/*.{ts,tsx}\" --fix",
"build": "npm run prepare && tshy && npm run generate-version && node scripts/add-shebang.cjs",
"format": "prettier --check \"./src/**/*.{ts,tsx,js,json,md}\" \"./test/**/*.{ts,tsx,js,json,md}\"",
"format:fix": "prettier --write \"./src/**/*.{ts,tsx,js,json,md}\" \"./test/**/*.{ts,tsx,js,json,md}\"",
"prepare": "husky && node .husky/setup-hooks.js",
"test": "vitest",
"build": "npm run prepare && tshy && npm run generate-version && node scripts/add-shebang.cjs",
"generate-version": "node scripts/build-helpers.cjs generate-version",
"inspect:build": "npm run build && npx @modelcontextprotocol/inspector -e MAPBOX_ACCESS_TOKEN=\"$MAPBOX_ACCESS_TOKEN\" node dist/esm/index.js",
"inspect:dev": "npx @modelcontextprotocol/inspector -e MAPBOX_ACCESS_TOKEN=\"$MAPBOX_ACCESS_TOKEN\" npx -y tsx src/index.ts",
"lint": "eslint \"./src/**/*.{ts,tsx}\" \"./test/**/*.{ts,tsx}\"",
"lint:fix": "eslint \"./src/**/*.{ts,tsx}\" \"./test/**/*.{ts,tsx}\" --fix",
"prepare": "husky && node .husky/setup-hooks.js",
"spellcheck": "cspell \"src/**/*.ts\" \"test/**/*.ts\"",
"sync-manifest": "node scripts/sync-manifest-version.cjs",
"dev:inspect": "npx @modelcontextprotocol/inspector -e MAPBOX_ACCESS_TOKEN=\"$MAPBOX_ACCESS_TOKEN\" npx -y tsx src/index.ts"
"test": "vitest"
},
"lint-staged": {
"*.{js,jsx,ts,tsx}": "eslint --fix",
Expand All @@ -32,6 +34,7 @@
"@typescript-eslint/eslint-plugin": "^8.0.0",
"@typescript-eslint/parser": "^8.0.0",
"@vitest/coverage-istanbul": "^3.2.4",
"cspell": "^9.2.1",
"eslint": "^9.0.0",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-n": "^17.21.3",
Expand Down
1 change: 1 addition & 0 deletions src/tools/MapboxApiBasedTool.schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,6 @@ export const OutputSchema = z.object({
})
])
),
structuredContent: z.record(z.unknown()).optional(),
isError: z.boolean().default(false)
});
28 changes: 23 additions & 5 deletions src/tools/MapboxApiBasedTool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,19 +68,37 @@ export abstract class MapboxApiBasedTool<
const input = this.inputSchema.parse(rawInput);
const result = await this.execute(input, accessToken);

// Check if result is already a content object (image or text)
// If result already has CallToolResult structure, return it directly
if (
result &&
typeof result === 'object' &&
(result.type === 'image' || result.type === 'text')
'content' in result &&
Array.isArray(result.content)
) {
return {
content: [result],
...result,
isError: result.isError ?? false
};
}

// Handle string results - return as plain text
if (typeof result === 'string') {
return {
content: [{ type: 'text', text: result }],
isError: false
};
}

// Handle object results - add to structuredContent and pretty-print as text
if (result && typeof result === 'object' && !Array.isArray(result)) {
return {
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
structuredContent: result as Record<string, unknown>,
isError: false
};
}

// Otherwise return as text
// Handle other types (arrays, primitives, null, undefined)
return {
content: [{ type: 'text', text: JSON.stringify(result) }],
isError: false
Expand Down Expand Up @@ -112,5 +130,5 @@ export abstract class MapboxApiBasedTool<
protected abstract execute(
_input: z.infer<InputSchema>,
accessToken: string
): Promise<any>;
): Promise<z.infer<typeof OutputSchema> | string | object>;
}
14 changes: 12 additions & 2 deletions src/tools/category-list-tool/CategoryListTool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,11 @@ export class CategoryListTool extends MapboxApiBasedTool<
protected async execute(
input: CategoryListInput,
accessToken: string
): Promise<unknown> {
): Promise<{
content: Array<{ type: 'text'; text: string }>;
structuredContent?: Record<string, unknown>;
isError?: boolean;
}> {
const url = new URL(
'https://api.mapbox.com/search/searchbox/v1/list/category'
);
Expand Down Expand Up @@ -80,8 +84,14 @@ export class CategoryListTool extends MapboxApiBasedTool<
.slice(startIndex, endIndex)
.map((item) => item.canonical_id);

return {
const resultData = {
listItems: categoryIds
};

return {
content: [{ type: 'text', text: JSON.stringify(resultData, null, 2) }],
structuredContent: resultData as Record<string, unknown>,
isError: false
};
}
}
18 changes: 15 additions & 3 deletions src/tools/category-search-tool/CategorySearchTool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,11 @@ export class CategorySearchTool extends MapboxApiBasedTool<
protected async execute(
input: z.infer<typeof CategorySearchInputSchema>,
accessToken: string
): Promise<{ type: 'text'; text: string }> {
): Promise<{
content: Array<{ type: 'text'; text: string }>;
structuredContent?: Record<string, unknown>;
isError?: boolean;
}> {
// Build URL with required parameters
const url = new URL(
`${MapboxApiBasedTool.mapboxApiEndpoint}search/searchbox/v1/category/${encodeURIComponent(input.category)}`
Expand Down Expand Up @@ -143,9 +147,17 @@ export class CategorySearchTool extends MapboxApiBasedTool<
const data = await response.json();

if (input.format === 'json_string') {
return { type: 'text', text: JSON.stringify(data, null, 2) };
return {
content: [{ type: 'text', text: JSON.stringify(data, null, 2) }],
structuredContent: data as Record<string, unknown>,
isError: false
};
} else {
return { type: 'text', text: this.formatGeoJsonToText(data) };
return {
content: [{ type: 'text', text: this.formatGeoJsonToText(data) }],
structuredContent: data as Record<string, unknown>,
isError: false
};
}
}
}
13 changes: 11 additions & 2 deletions src/tools/directions-tool/DirectionsTool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,11 @@ export class DirectionsTool extends MapboxApiBasedTool<
protected async execute(
input: z.infer<typeof DirectionsInputSchema>,
accessToken: string
): Promise<unknown> {
): Promise<{
content: Array<{ type: 'text'; text: string }>;
structuredContent?: Record<string, unknown>;
isError?: boolean;
}> {
// Validate exclude parameter against the actual routing_profile
// This is needed because some exclusions are only driving specific
if (input.exclude) {
Expand Down Expand Up @@ -194,6 +198,11 @@ export class DirectionsTool extends MapboxApiBasedTool<
}

const data = await response.json();
return cleanResponseData(input, data);
const cleanedData = cleanResponseData(input, data);
return {
content: [{ type: 'text', text: JSON.stringify(cleanedData, null, 2) }],
structuredContent: cleanedData as Record<string, unknown>,
isError: false
};
}
}
12 changes: 10 additions & 2 deletions src/tools/isochrone-tool/IsochroneTool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,11 @@ export class IsochroneTool extends MapboxApiBasedTool<
protected async execute(
input: z.infer<typeof IsochroneInputSchema>,
accessToken: string
): Promise<unknown> {
): Promise<{
content: Array<{ type: 'text'; text: string }>;
structuredContent?: Record<string, unknown>;
isError?: boolean;
}> {
const url = new URL(
`${MapboxApiBasedTool.mapboxApiEndpoint}isochrone/v1/${input.profile}/${input.coordinates.longitude}%2C${input.coordinates.latitude}`
);
Expand Down Expand Up @@ -89,6 +93,10 @@ export class IsochroneTool extends MapboxApiBasedTool<
}

const data = await response.json();
return data;
return {
content: [{ type: 'text', text: JSON.stringify(data) }],
structuredContent: data as Record<string, unknown>,
isError: false
};
}
}
12 changes: 10 additions & 2 deletions src/tools/matrix-tool/MatrixTool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,11 @@ export class MatrixTool extends MapboxApiBasedTool<typeof MatrixInputSchema> {
protected async execute(
input: z.infer<typeof MatrixInputSchema>,
accessToken: string
): Promise<unknown> {
): Promise<{
content: Array<{ type: 'text'; text: string }>;
structuredContent?: Record<string, unknown>;
isError?: boolean;
}> {
// Validate input based on profile type
if (input.profile === 'driving-traffic' && input.coordinates.length > 10) {
throw new Error(
Expand Down Expand Up @@ -214,6 +218,10 @@ export class MatrixTool extends MapboxApiBasedTool<typeof MatrixInputSchema> {

// Return the matrix data
const data = await response.json();
return data;
return {
content: [{ type: 'text', text: JSON.stringify(data, null, 2) }],
structuredContent: data as Record<string, unknown>,
isError: false
};
}
}
27 changes: 21 additions & 6 deletions src/tools/reverse-geocode-tool/ReverseGeocodeTool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,11 @@ export class ReverseGeocodeTool extends MapboxApiBasedTool<
protected async execute(
input: z.infer<typeof ReverseGeocodeInputSchema>,
accessToken: string
): Promise<{ type: 'text'; text: string }> {
): Promise<{
content: Array<{ type: 'text'; text: string }>;
structuredContent?: Record<string, unknown>;
isError?: boolean;
}> {
// When limit > 1, must specify exactly one type
if (
input.limit &&
Expand Down Expand Up @@ -120,17 +124,28 @@ export class ReverseGeocodeTool extends MapboxApiBasedTool<
);
}

const data = (await response.json()) as any;
const data = (await response.json()) as Record<string, unknown>;

// Check if the response has features
if (!data || !data.features || data.features.length === 0) {
return { type: 'text', text: 'No results found.' };
if (!data || !Array.isArray(data.features) || data.features.length === 0) {
return {
content: [{ type: 'text', text: 'No results found.' }],
isError: false
};
}

if (input.format === 'json_string') {
return { type: 'text', text: JSON.stringify(data, null, 2) };
return {
content: [{ type: 'text', text: JSON.stringify(data, null, 2) }],
structuredContent: data as Record<string, unknown>,
isError: false
};
} else {
return { type: 'text', text: this.formatGeoJsonToText(data) };
return {
content: [{ type: 'text', text: this.formatGeoJsonToText(data) }],
structuredContent: data as Record<string, unknown>,
isError: false
};
}
}
}
14 changes: 11 additions & 3 deletions src/tools/search-and-geocode-tool/SearchAndGeocodeTool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,11 @@ export class SearchAndGeocodeTool extends MapboxApiBasedTool<
protected async execute(
input: z.infer<typeof SearchAndGeocodeInputSchema>,
accessToken: string
): Promise<{ type: 'text'; text: string }> {
): Promise<{
content: Array<{ type: 'text'; text: string }>;
structuredContent?: Record<string, unknown>;
isError?: boolean;
}> {
this.log(
'info',
`SearchAndGeocodeTool: Starting search with input: ${JSON.stringify(input)}`
Expand Down Expand Up @@ -177,9 +181,13 @@ export class SearchAndGeocodeTool extends MapboxApiBasedTool<
const data = await response.json();
this.log(
'info',
`SearchAndGeocodeTool: Successfully completed search, found ${(data as any).features?.length || 0} results`
`SearchAndGeocodeTool: Successfully completed search, found ${(data as unknown as any).features?.length || 0} results`
);

return { type: 'text', text: this.formatGeoJsonToText(data) };
return {
content: [{ type: 'text', text: this.formatGeoJsonToText(data) }],
structuredContent: data as Record<string, unknown>,
isError: false
};
}
}
16 changes: 12 additions & 4 deletions src/tools/static-map-image-tool/StaticMapImageTool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,10 @@ export class StaticMapImageTool extends MapboxApiBasedTool<
protected async execute(
input: z.infer<typeof StaticMapImageInputSchema>,
accessToken: string
): Promise<unknown> {
): Promise<{
content: Array<{ type: 'image'; data: string; mimeType: string }>;
isError?: boolean;
}> {
const { longitude: lng, latitude: lat } = input.center;
const { width, height } = input.size;

Expand Down Expand Up @@ -111,9 +114,14 @@ export class StaticMapImageTool extends MapboxApiBasedTool<
const mimeType = isRasterStyle ? 'image/jpeg' : 'image/png';

return {
type: 'image',
data: base64Data,
mimeType
content: [
{
type: 'image',
data: base64Data,
mimeType
}
],
isError: false
};
}
}