diff --git a/.changeset/server-user-agent.md b/.changeset/server-user-agent.md new file mode 100644 index 0000000..c809557 --- /dev/null +++ b/.changeset/server-user-agent.md @@ -0,0 +1,5 @@ +--- +"@taskade/mcp-server": patch +--- + +Send `User-Agent: taskade-mcp-server/` on every Taskade API request, v1 and v2. The server identifies itself, so the Taskade team can measure the usage of this package and debug its requests. The header holds no user data. Requests used the generic `node-fetch` value before. diff --git a/packages/server/src/server.test.ts b/packages/server/src/server.test.ts new file mode 100644 index 0000000..7e21407 --- /dev/null +++ b/packages/server/src/server.test.ts @@ -0,0 +1,65 @@ +import { readFileSync } from 'node:fs'; + +import { Client } from '@modelcontextprotocol/sdk/client/index.js'; +import { InMemoryTransport } from '@modelcontextprotocol/sdk/inMemory.js'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; + +import { TaskadeMCPServer } from './server'; + +const fetchMock = vi.hoisted(() => vi.fn()); + +vi.mock('node-fetch', () => ({ default: fetchMock })); + +const packageVersion: string = JSON.parse( + readFileSync(new URL('../package.json', import.meta.url), 'utf8'), +).version; + +describe('TaskadeMCPServer API requests', () => { + let client: Client; + + beforeEach(async () => { + fetchMock.mockReset(); + fetchMock.mockResolvedValue({ + ok: true, + status: 200, + statusText: 'OK', + json: async () => ({ ok: true }), + text: async () => '{"ok":true}', + }); + + const server = new TaskadeMCPServer({ accessToken: 'test-access-token' }); + client = new Client({ name: 'test-client', version: '0.0.0' }); + const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); + await Promise.all([server.connect(serverTransport), client.connect(clientTransport)]); + }); + + afterEach(async () => { + await client.close(); + }); + + it.each([ + { + api: 'v1', + name: 'workspacesGet', + args: {}, + url: 'https://www.taskade.com/api/v1/workspaces', + }, + { + api: 'v2', + name: 'promptAgent', + args: { spaceId: 's1', agentId: 'a1', prompt: 'hi' }, + url: 'https://www.taskade.com/api/v2/promptAgent', + }, + ])('names the package and its version on a $api tool call', async ({ name, args, url }) => { + const result = await client.callTool({ name, arguments: args }); + + expect(result.isError).toBeFalsy(); + expect(packageVersion).toMatch(/^\d+\.\d+\.\d+/); + expect(fetchMock).toHaveBeenCalledTimes(1); + + const [calledUrl, init] = fetchMock.mock.calls[0]; + expect(calledUrl).toBe(url); + expect(init.headers['User-Agent']).toBe(`taskade-mcp-server/${packageVersion}`); + expect(init.headers.Authorization).toBe('Bearer test-access-token'); + }); +}); diff --git a/packages/server/src/server.ts b/packages/server/src/server.ts index 4249508..8a7116b 100644 --- a/packages/server/src/server.ts +++ b/packages/server/src/server.ts @@ -5,6 +5,11 @@ import { version } from '../package.json'; import { setupTools } from './tools.generated'; import { setupToolsV2 } from './tools.v2.generated'; +// Every API request names this package and its version. With this header, Taskade can measure +// the usage of the package and debug its requests. The build inlines `version` from +// package.json into bin/cli.mjs. +const USER_AGENT = `taskade-mcp-server/${version}`; + type TaskadeServerOpts = { accessToken: string; }; @@ -29,6 +34,7 @@ export class TaskadeMCPServer extends McpServer { fetch, headers: { Authorization: `Bearer ${this.config.accessToken}`, + 'User-Agent': USER_AGENT, }, normalizeResponse: { folderProjectsGet: (response) => { @@ -254,6 +260,7 @@ export class TaskadeMCPServer extends McpServer { fetch, headers: { Authorization: `Bearer ${this.config.accessToken}`, + 'User-Agent': USER_AGENT, }, normalizeResponse: { promptAgent: (response) => {