From 5d89bc6fcfb08fca0aa39a2272a44d5a82a75cbd Mon Sep 17 00:00:00 2001 From: johnxie Date: Fri, 16 Oct 2026 23:57:27 -0700 Subject: [PATCH] feat(server): send a User-Agent that names the package and version (#100) The server sent only the Authorization header to the Taskade API. The bundled HTTP client then added its generic default, "node-fetch". The v1 and the v2 tool setup now send "User-Agent: taskade-mcp-server/". The build inlines the version from package.json into bin/cli.mjs. A unit test asserts the header and the version on a v1 and on a v2 tool call. Closes #100 --- .changeset/server-user-agent.md | 5 +++ packages/server/src/server.test.ts | 65 ++++++++++++++++++++++++++++++ packages/server/src/server.ts | 7 ++++ 3 files changed, 77 insertions(+) create mode 100644 .changeset/server-user-agent.md create mode 100644 packages/server/src/server.test.ts 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) => {