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
5 changes: 5 additions & 0 deletions .changeset/server-user-agent.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@taskade/mcp-server": patch
---

Send `User-Agent: taskade-mcp-server/<version>` 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.
65 changes: 65 additions & 0 deletions packages/server/src/server.test.ts
Original file line number Diff line number Diff line change
@@ -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');
});
});
7 changes: 7 additions & 0 deletions packages/server/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};
Expand All @@ -29,6 +34,7 @@ export class TaskadeMCPServer extends McpServer {
fetch,
headers: {
Authorization: `Bearer ${this.config.accessToken}`,
'User-Agent': USER_AGENT,
},
normalizeResponse: {
folderProjectsGet: (response) => {
Expand Down Expand Up @@ -254,6 +260,7 @@ export class TaskadeMCPServer extends McpServer {
fetch,
headers: {
Authorization: `Bearer ${this.config.accessToken}`,
'User-Agent': USER_AGENT,
},
normalizeResponse: {
promptAgent: (response) => {
Expand Down
Loading