Skip to content
Merged
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
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@mapbox/mcp-devkit-server",
"version": "0.2.3",
"version": "0.3.0",
"description": "Mapbox MCP devkit server",
"main": "dist/index.js",
"module": "dist/index-esm.js",
Expand Down
5 changes: 3 additions & 2 deletions plop-templates/mapbox-api-tool.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -36,14 +36,15 @@ export class {{pascalCase name}}Tool extends MapboxApiBasedTool<
}

protected async execute(
input: {{pascalCase name}}Input
input: {{pascalCase name}}Input,
accessToken?: string
): Promise<{ type: 'text'; text: string }> {
try {
// TODO: Implement your Mapbox API call here

// Example implementation:
// const username = MapboxApiBasedTool.getUserNameFromToken();
// const url = `${MapboxApiBasedTool.MAPBOX_API_ENDPOINT}styles/v1/${username}/${input.styleId}?access_token=${MapboxApiBasedTool.MAPBOX_ACCESS_TOKEN}`;
// const url = `${MapboxApiBasedTool.MAPBOX_API_ENDPOINT}styles/v1/${username}/${input.styleId}?access_token=${accessToken}`;
//
// const response = await fetch(url);
//
Expand Down
15 changes: 11 additions & 4 deletions src/tools/BaseTool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
McpServer,
RegisteredTool
} from '@modelcontextprotocol/sdk/server/mcp';
import { RequestHandlerExtra } from '@modelcontextprotocol/sdk/shared/protocol.js';
import { z, ZodTypeAny } from 'zod';

const ContentItemSchema = z.union([
Expand Down Expand Up @@ -38,10 +39,15 @@
/**
* Validates and runs the tool logic.
*/
async run(rawInput: unknown): Promise<z.infer<typeof OutputSchema>> {
async run(
rawInput: unknown,
extra?: RequestHandlerExtra<any, any>

Check warning on line 44 in src/tools/BaseTool.ts

View workflow job for this annotation

GitHub Actions / test

Unexpected any. Specify a different type

Check warning on line 44 in src/tools/BaseTool.ts

View workflow job for this annotation

GitHub Actions / test

Unexpected any. Specify a different type
Copy link

@ivan-yankavets ivan-yankavets Aug 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@zmofei , can we have this somehow in the base class so that we don't need to pass it to every tool?
this supposed to remove some code duplication.

run could store token in a property and exec simply use it as it was with MAPBOX_ACCESS_TOKEN

): Promise<z.infer<typeof OutputSchema>> {
try {
const input = this.inputSchema.parse(rawInput);
const result = await this.execute(input);
const accessToken =
extra?.authInfo?.token || process.env.MAPBOX_ACCESS_TOKEN;
const result = await this.execute(input, accessToken);

// Check if result is already a content object (image or text)
if (
Expand Down Expand Up @@ -86,7 +92,8 @@
* Tool logic to be implemented by subclasses.
*/
protected abstract execute(
_input: z.infer<InputSchema>
_input: z.infer<InputSchema>,
accessToken?: string
): Promise<ContentItem | unknown>;

/**
Expand All @@ -99,7 +106,7 @@
this.description,
(this.inputSchema as unknown as z.ZodObject<Record<string, z.ZodTypeAny>>)
.shape,
this.run.bind(this)
(args, extra) => this.run(args, extra)
);
}

Expand Down
2 changes: 1 addition & 1 deletion src/tools/MapboxApiBasedTool.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ describe('MapboxApiBasedTool', () => {
});

expect(() => MapboxApiBasedTool.getUserNameFromToken()).toThrow(
'MAPBOX_ACCESS_TOKEN is not set'
'No access token provided. Please set MAPBOX_ACCESS_TOKEN environment variable or pass it as an argument.'
);
} finally {
Object.defineProperty(MapboxApiBasedTool, 'MAPBOX_ACCESS_TOKEN', {
Expand Down
35 changes: 26 additions & 9 deletions src/tools/MapboxApiBasedTool.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { RequestHandlerExtra } from '@modelcontextprotocol/sdk/shared/protocol.js';
import { z, ZodTypeAny } from 'zod';
import { BaseTool, OutputSchema } from './BaseTool.js';

Expand All @@ -17,14 +18,19 @@
* Mapbox tokens are JWT tokens where the payload contains the username.
* @throws Error if the token is not set, invalid, or doesn't contain username
*/
static getUserNameFromToken(): string {
if (!MapboxApiBasedTool.MAPBOX_ACCESS_TOKEN) {
throw new Error('MAPBOX_ACCESS_TOKEN is not set');
static getUserNameFromToken(access_token?: string): string {
if (!access_token) {
if (!MapboxApiBasedTool.MAPBOX_ACCESS_TOKEN) {
throw new Error(
'No access token provided. Please set MAPBOX_ACCESS_TOKEN environment variable or pass it as an argument.'
);
}
access_token = MapboxApiBasedTool.MAPBOX_ACCESS_TOKEN;
}

try {
// JWT format: header.payload.signature
const parts = MapboxApiBasedTool.MAPBOX_ACCESS_TOKEN.split('.');
const parts = access_token.split('.');
if (parts.length !== 3) {
throw new Error('MAPBOX_ACCESS_TOKEN is not in valid JWT format');
}
Expand Down Expand Up @@ -68,19 +74,30 @@
/**
* Validates Mapbox token and runs the tool logic.
*/
async run(rawInput: unknown): Promise<z.infer<typeof OutputSchema>> {
async run(
rawInput: unknown,
extra?: RequestHandlerExtra<any, any>

Check warning on line 79 in src/tools/MapboxApiBasedTool.ts

View workflow job for this annotation

GitHub Actions / test

Unexpected any. Specify a different type

Check warning on line 79 in src/tools/MapboxApiBasedTool.ts

View workflow job for this annotation

GitHub Actions / test

Unexpected any. Specify a different type
): Promise<z.infer<typeof OutputSchema>> {
try {
if (!MapboxApiBasedTool.MAPBOX_ACCESS_TOKEN) {
throw new Error('MAPBOX_ACCESS_TOKEN is not set');
// First check if token is provided via authentication context
// Check both standard token field and accessToken in extra for compatibility
// In the streamableHttp, the authInfo is injected into extra from `req.auth`
// https://github.com/modelcontextprotocol/typescript-sdk/blob/main/src/server/streamableHttp.ts#L405
const authToken = extra?.authInfo?.token;
const accessToken = authToken || MapboxApiBasedTool.MAPBOX_ACCESS_TOKEN;
if (!accessToken) {
throw new Error(
'No access token available. Please provide via Bearer auth or MAPBOX_ACCESS_TOKEN env var'
);
}

// Validate that the token has the correct JWT format
if (!this.isValidJwtFormat(MapboxApiBasedTool.MAPBOX_ACCESS_TOKEN)) {
if (!this.isValidJwtFormat(accessToken)) {
throw new Error('MAPBOX_ACCESS_TOKEN is not in valid JWT format');
}

// Call parent run method which handles the rest
return await super.run(rawInput);
return await super.run(rawInput, extra);
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : String(error);
Expand Down
9 changes: 6 additions & 3 deletions src/tools/create-style-tool/CreateStyleTool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,12 @@
super({ inputSchema: CreateStyleSchema });
}

protected async execute(input: CreateStyleInput): Promise<any> {
const username = MapboxApiBasedTool.getUserNameFromToken();
const url = `${MapboxApiBasedTool.MAPBOX_API_ENDPOINT}styles/v1/${username}?access_token=${MapboxApiBasedTool.MAPBOX_ACCESS_TOKEN}`;
protected async execute(
input: CreateStyleInput,
accessToken?: string
): Promise<any> {

Check warning on line 20 in src/tools/create-style-tool/CreateStyleTool.ts

View workflow job for this annotation

GitHub Actions / test

Unexpected any. Specify a different type
const username = MapboxApiBasedTool.getUserNameFromToken(accessToken);
const url = `${MapboxApiBasedTool.MAPBOX_API_ENDPOINT}styles/v1/${username}?access_token=${accessToken}`;

const payload = {
name: input.name,
Expand Down
17 changes: 16 additions & 1 deletion src/tools/create-token-tool/CreateTokenTool.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,17 +79,31 @@ describe('CreateTokenTool', () => {

it('throws error when unable to extract username from token', async () => {
const originalToken = MapboxApiBasedTool.MAPBOX_ACCESS_TOKEN;
const originalEnvToken = process.env.MAPBOX_ACCESS_TOKEN;

try {
// Set a token without username in payload
const invalidPayload = Buffer.from(
JSON.stringify({ sub: 'test' })
).toString('base64');
const invalidToken = `eyJhbGciOiJIUzI1NiJ9.${invalidPayload}.signature`;

Object.defineProperty(MapboxApiBasedTool, 'MAPBOX_ACCESS_TOKEN', {
value: `eyJhbGciOiJIUzI1NiJ9.${invalidPayload}.signature`,
value: invalidToken,
writable: true,
configurable: true
});
process.env.MAPBOX_ACCESS_TOKEN = invalidToken;

// Setup fetch mock to prevent actual API calls
const fetchMock = setupFetch();
fetchMock.mockResolvedValueOnce({
ok: true,
status: 200,
statusText: 'OK',
headers: new Headers(),
json: async () => ({ token: 'test-token' })
} as Response);

const toolWithInvalidToken = new CreateTokenTool();
toolWithInvalidToken['log'] = jest.fn();
Expand All @@ -112,6 +126,7 @@ describe('CreateTokenTool', () => {
writable: true,
configurable: true
});
process.env.MAPBOX_ACCESS_TOKEN = originalEnvToken;
}
});
});
Expand Down
9 changes: 5 additions & 4 deletions src/tools/create-token-tool/CreateTokenTool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,14 @@ export class CreateTokenTool extends MapboxApiBasedTool<
}

protected async execute(
input: CreateTokenInput
input: CreateTokenInput,
accessToken?: string
): Promise<{ type: 'text'; text: string }> {
if (!MapboxApiBasedTool.MAPBOX_ACCESS_TOKEN) {
if (!accessToken) {
throw new Error('MAPBOX_ACCESS_TOKEN is not set');
}

const username = MapboxApiBasedTool.getUserNameFromToken();
const username = MapboxApiBasedTool.getUserNameFromToken(accessToken);

this.log(
'info',
Expand All @@ -47,7 +48,7 @@ export class CreateTokenTool extends MapboxApiBasedTool<
);
}

const url = `${MapboxApiBasedTool.MAPBOX_API_ENDPOINT}tokens/v2/${username}?access_token=${MapboxApiBasedTool.MAPBOX_ACCESS_TOKEN}`;
const url = `${MapboxApiBasedTool.MAPBOX_API_ENDPOINT}tokens/v2/${username}?access_token=${accessToken}`;

const body: {
note: string;
Expand Down
9 changes: 6 additions & 3 deletions src/tools/delete-style-tool/DeleteStyleTool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,12 @@
super({ inputSchema: DeleteStyleSchema });
}

protected async execute(input: DeleteStyleInput): Promise<any> {
const username = MapboxApiBasedTool.getUserNameFromToken();
const url = `${MapboxApiBasedTool.MAPBOX_API_ENDPOINT}styles/v1/${username}/${input.styleId}?access_token=${MapboxApiBasedTool.MAPBOX_ACCESS_TOKEN}`;
protected async execute(
input: DeleteStyleInput,
accessToken?: string
): Promise<any> {

Check warning on line 20 in src/tools/delete-style-tool/DeleteStyleTool.ts

View workflow job for this annotation

GitHub Actions / test

Unexpected any. Specify a different type
const username = MapboxApiBasedTool.getUserNameFromToken(accessToken);
const url = `${MapboxApiBasedTool.MAPBOX_API_ENDPOINT}styles/v1/${username}/${input.styleId}?access_token=${accessToken}`;

const response = await fetch(url, {
method: 'DELETE'
Expand Down
11 changes: 7 additions & 4 deletions src/tools/list-styles-tool/ListStylesTool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,18 @@
super({ inputSchema: ListStylesSchema });
}

protected async execute(input: ListStylesInput): Promise<any> {
const username = MapboxApiBasedTool.getUserNameFromToken();
protected async execute(
input: ListStylesInput,
accessToken?: string
): Promise<any> {

Check warning on line 17 in src/tools/list-styles-tool/ListStylesTool.ts

View workflow job for this annotation

GitHub Actions / test

Unexpected any. Specify a different type
const username = MapboxApiBasedTool.getUserNameFromToken(accessToken);

// Build query parameters
const params = new URLSearchParams();
if (!MapboxApiBasedTool.MAPBOX_ACCESS_TOKEN) {
if (!accessToken) {
throw new Error('MAPBOX_ACCESS_TOKEN is not set');
}
params.append('access_token', MapboxApiBasedTool.MAPBOX_ACCESS_TOKEN);
params.append('access_token', accessToken);

if (input.limit) {
params.append('limit', input.limit.toString());
Expand Down
17 changes: 16 additions & 1 deletion src/tools/list-tokens-tool/ListTokensTool.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,17 +70,31 @@ describe('ListTokensTool', () => {

it('throws error when unable to extract username from token', async () => {
const originalToken = MapboxApiBasedTool.MAPBOX_ACCESS_TOKEN;
const originalEnvToken = process.env.MAPBOX_ACCESS_TOKEN;

try {
// Set a token without username in payload
const invalidPayload = Buffer.from(
JSON.stringify({ sub: 'test' })
).toString('base64');
const invalidToken = `eyJhbGciOiJIUzI1NiJ9.${invalidPayload}.signature`;

Object.defineProperty(MapboxApiBasedTool, 'MAPBOX_ACCESS_TOKEN', {
value: `eyJhbGciOiJIUzI1NiJ9.${invalidPayload}.signature`,
value: invalidToken,
writable: true,
configurable: true
});
process.env.MAPBOX_ACCESS_TOKEN = invalidToken;

// Setup fetch mock to prevent actual API calls
const fetchMock = setupFetch();
fetchMock.mockResolvedValueOnce({
ok: true,
status: 200,
statusText: 'OK',
headers: new Headers(),
json: async () => []
} as Response);

const toolWithInvalidToken = new ListTokensTool();
toolWithInvalidToken['log'] = jest.fn();
Expand All @@ -100,6 +114,7 @@ describe('ListTokensTool', () => {
writable: true,
configurable: true
});
process.env.MAPBOX_ACCESS_TOKEN = originalEnvToken;
}
});
});
Expand Down
14 changes: 6 additions & 8 deletions src/tools/list-tokens-tool/ListTokensTool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,14 @@ export class ListTokensTool extends MapboxApiBasedTool<
}

protected async execute(
input: ListTokensInput
input: ListTokensInput,
accessToken?: string
): Promise<{ type: 'text'; text: string }> {
if (!MapboxApiBasedTool.MAPBOX_ACCESS_TOKEN) {
if (!accessToken) {
throw new Error('MAPBOX_ACCESS_TOKEN is not set');
}

const username = MapboxApiBasedTool.getUserNameFromToken();
const username = MapboxApiBasedTool.getUserNameFromToken(accessToken);

this.log(
'info',
Expand All @@ -28,7 +29,7 @@ export class ListTokensTool extends MapboxApiBasedTool<

// Build initial query parameters
const params = new URLSearchParams();
params.append('access_token', MapboxApiBasedTool.MAPBOX_ACCESS_TOKEN);
params.append('access_token', accessToken);

if (input.default !== undefined) {
params.append('default', String(input.default));
Expand Down Expand Up @@ -103,10 +104,7 @@ export class ListTokensTool extends MapboxApiBasedTool<
// Ensure the next URL includes the access token
const nextUrl = new URL(links.next);
if (!nextUrl.searchParams.has('access_token')) {
nextUrl.searchParams.append(
'access_token',
MapboxApiBasedTool.MAPBOX_ACCESS_TOKEN
);
nextUrl.searchParams.append('access_token', accessToken);
}
const nextUrlString = nextUrl.toString();

Expand Down
9 changes: 6 additions & 3 deletions src/tools/retrieve-style-tool/RetrieveStyleTool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,12 @@
super({ inputSchema: RetrieveStyleSchema });
}

protected async execute(input: RetrieveStyleInput): Promise<any> {
const username = MapboxApiBasedTool.getUserNameFromToken();
const url = `${MapboxApiBasedTool.MAPBOX_API_ENDPOINT}styles/v1/${username}/${input.styleId}?access_token=${MapboxApiBasedTool.MAPBOX_ACCESS_TOKEN}`;
protected async execute(
input: RetrieveStyleInput,
accessToken?: string
): Promise<any> {

Check warning on line 20 in src/tools/retrieve-style-tool/RetrieveStyleTool.ts

View workflow job for this annotation

GitHub Actions / test

Unexpected any. Specify a different type
const username = MapboxApiBasedTool.getUserNameFromToken(accessToken);
const url = `${MapboxApiBasedTool.MAPBOX_API_ENDPOINT}styles/v1/${username}/${input.styleId}?access_token=${accessToken}`;

const response = await fetch(url);

Expand Down
9 changes: 6 additions & 3 deletions src/tools/update-style-tool/UpdateStyleTool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,12 @@ export class UpdateStyleTool extends MapboxApiBasedTool<
super({ inputSchema: UpdateStyleSchema });
}

protected async execute(input: UpdateStyleInput): Promise<any> {
const username = MapboxApiBasedTool.getUserNameFromToken();
const url = `${MapboxApiBasedTool.MAPBOX_API_ENDPOINT}styles/v1/${username}/${input.styleId}?access_token=${MapboxApiBasedTool.MAPBOX_ACCESS_TOKEN}`;
protected async execute(
input: UpdateStyleInput,
accessToken?: string
): Promise<any> {
const username = MapboxApiBasedTool.getUserNameFromToken(accessToken);
const url = `${MapboxApiBasedTool.MAPBOX_API_ENDPOINT}styles/v1/${username}/${input.styleId}?access_token=${accessToken}`;

const payload: any = {};
if (input.name) payload.name = input.name;
Expand Down
Loading