From fc1e7a8edc3219f937f6a2a4947962a57ef714fa Mon Sep 17 00:00:00 2001 From: BatLeDev Date: Wed, 1 Jul 2026 13:54:40 +0200 Subject: [PATCH] feat: tag outgoing data-fair API calls with a /mcp referer for traffic classification --- src/mcp-servers/datasets/tools.test.ts | 28 +++++++++++++++++-- src/mcp-servers/datasets/tools/_utils.ts | 5 ++-- .../datasets/tools/aggregate-data.ts | 2 +- .../datasets/tools/calculate-metric.ts | 2 +- .../datasets/tools/describe-dataset.ts | 4 +-- .../datasets/tools/get-field-values.ts | 2 +- src/mcp-servers/datasets/tools/search-data.ts | 2 +- .../datasets/tools/search-datasets.ts | 2 +- 8 files changed, 36 insertions(+), 11 deletions(-) diff --git a/src/mcp-servers/datasets/tools.test.ts b/src/mcp-servers/datasets/tools.test.ts index 979b1b3..414ddca 100644 --- a/src/mcp-servers/datasets/tools.test.ts +++ b/src/mcp-servers/datasets/tools.test.ts @@ -6,12 +6,14 @@ import { Client } from '@modelcontextprotocol/sdk/client/index.js' import { InMemoryTransport } from '@modelcontextprotocol/sdk/inMemory.js' import nock from 'nock' import registerTools from './tools.ts' +import { buildAxiosOptions } from './tools/_utils.ts' +import config from '#config' /** * Fake Data Fair API server that returns canned responses based on the URL path. * Routes are matched longest-pattern-first to avoid ambiguity. */ -const routes: Record any> = {} +const routes: Record any> = {} const fakeApi = createServer((req: IncomingMessage, res: ServerResponse) => { const url = new URL(req.url!, 'http://localhost') @@ -19,7 +21,7 @@ const fakeApi = createServer((req: IncomingMessage, res: ServerResponse) => { const sortedPatterns = Object.keys(routes).sort((a, b) => b.length - a.length) for (const pattern of sortedPatterns) { if (url.pathname.includes(pattern)) { - const body = JSON.stringify(routes[pattern](url)) + const body = JSON.stringify(routes[pattern](url, req)) res.writeHead(200, { 'content-type': 'application/json' }) res.end(body) return @@ -72,6 +74,28 @@ after(async () => { await new Promise(resolve => fakeApi.close(() => resolve())) }) +describe('Referer header on outgoing data-fair calls', () => { + // The Referer is only meant to tag traffic when the MCP is self-hosted as part + // of the data-fair stack (no portalUrl, origin resolved from X-Forwarded-* headers). + // In standalone mode (portalUrl set, remote data-fair) no Referer is sent. + it('sets a Referer ending in /mcp when self-hosted (stack mode, no portalUrl)', () => { + const saved = config.portalUrl + config.portalUrl = undefined + try { + const headers = buildAxiosOptions('https://data.example.com').headers as Record + assert.equal(headers.Referer, 'https://data.example.com/mcp') + } finally { + config.portalUrl = saved + } + }) + + it('omits the Referer in standalone mode (portalUrl set)', () => { + // portalUrl is set by the `before` hook above (standalone mode) + const headers = buildAxiosOptions('https://data.example.com').headers as Record + assert.equal(headers.Referer, undefined) + }) +}) + describe('list_datasets', () => { it('should list datasets and return formatted results', async () => { routes['/catalog/datasets'] = (url) => ({ diff --git a/src/mcp-servers/datasets/tools/_utils.ts b/src/mcp-servers/datasets/tools/_utils.ts index d0160dc..456956f 100644 --- a/src/mcp-servers/datasets/tools/_utils.ts +++ b/src/mcp-servers/datasets/tools/_utils.ts @@ -54,10 +54,11 @@ export const getOrigin = (headers: IsomorphicHeaders | undefined): string => { } } -export const buildAxiosOptions = (headers: IsomorphicHeaders | undefined): AxiosRequestConfig => { +export const buildAxiosOptions = (baseUrl: string): AxiosRequestConfig => { const axiosHeaders: Record = { - 'User-Agent': '@data-fair/mcp (Datasets)' + 'User-Agent': '@data-fair/mcp (Datasets)', } + if (!config.portalUrl) axiosHeaders['Referer'] = baseUrl + '/mcp' if (config.dataFairAPIKey) { axiosHeaders['x-api-key'] = config.dataFairAPIKey } diff --git a/src/mcp-servers/datasets/tools/aggregate-data.ts b/src/mcp-servers/datasets/tools/aggregate-data.ts index 1702216..5eedcd9 100644 --- a/src/mcp-servers/datasets/tools/aggregate-data.ts +++ b/src/mcp-servers/datasets/tools/aggregate-data.ts @@ -56,7 +56,7 @@ export default (server: McpServer) => { try { response = (await axios.get( fetchUrl.toString(), - buildAxiosOptions(extra.requestInfo?.headers) + buildAxiosOptions(baseUrl) )).data } catch (err: any) { handleApiError(err) diff --git a/src/mcp-servers/datasets/tools/calculate-metric.ts b/src/mcp-servers/datasets/tools/calculate-metric.ts index 62d167f..7256288 100644 --- a/src/mcp-servers/datasets/tools/calculate-metric.ts +++ b/src/mcp-servers/datasets/tools/calculate-metric.ts @@ -49,7 +49,7 @@ export default (server: McpServer) => { try { response = (await axios.get( fetchUrl.toString(), - buildAxiosOptions(extra.requestInfo?.headers) + buildAxiosOptions(baseUrl) )).data } catch (err: any) { handleApiError(err) diff --git a/src/mcp-servers/datasets/tools/describe-dataset.ts b/src/mcp-servers/datasets/tools/describe-dataset.ts index 469367a..6741631 100644 --- a/src/mcp-servers/datasets/tools/describe-dataset.ts +++ b/src/mcp-servers/datasets/tools/describe-dataset.ts @@ -67,7 +67,7 @@ export default (server: McpServer) => { try { fetchedData = (await axios.get( new URL(`/data-fair/api/v1/datasets/${encodeURIComponent(params.datasetId)}`, baseUrl).toString(), - buildAxiosOptions(extra.requestInfo?.headers) + buildAxiosOptions(baseUrl) )).data } catch (err: any) { handleApiError(err) @@ -81,7 +81,7 @@ export default (server: McpServer) => { try { sampleLines = (await axios.get( sampleUrl.toString(), - buildAxiosOptions(extra.requestInfo?.headers) + buildAxiosOptions(baseUrl) )).data.results } catch (err: any) { handleApiError(err) diff --git a/src/mcp-servers/datasets/tools/get-field-values.ts b/src/mcp-servers/datasets/tools/get-field-values.ts index c858184..fc1e66c 100644 --- a/src/mcp-servers/datasets/tools/get-field-values.ts +++ b/src/mcp-servers/datasets/tools/get-field-values.ts @@ -48,7 +48,7 @@ export default (server: McpServer) => { try { values = (await axios.get( fetchUrl.toString(), - buildAxiosOptions(extra.requestInfo?.headers) + buildAxiosOptions(baseUrl) )).data } catch (err: any) { handleApiError(err) diff --git a/src/mcp-servers/datasets/tools/search-data.ts b/src/mcp-servers/datasets/tools/search-data.ts index b8fe227..e52390c 100644 --- a/src/mcp-servers/datasets/tools/search-data.ts +++ b/src/mcp-servers/datasets/tools/search-data.ts @@ -71,7 +71,7 @@ export default (server: McpServer) => { try { response = (await axios.get( fetchUrlStr, - buildAxiosOptions(extra.requestInfo?.headers) + buildAxiosOptions(baseUrl) )).data } catch (err: any) { handleApiError(err) diff --git a/src/mcp-servers/datasets/tools/search-datasets.ts b/src/mcp-servers/datasets/tools/search-datasets.ts index d4e7d54..10092ed 100644 --- a/src/mcp-servers/datasets/tools/search-datasets.ts +++ b/src/mcp-servers/datasets/tools/search-datasets.ts @@ -45,7 +45,7 @@ export default (server: McpServer) => { try { fetchedData = (await axios.get( fetchUrl.toString(), - buildAxiosOptions(extra.requestInfo?.headers) + buildAxiosOptions(baseUrl) )).data } catch (err: any) { handleApiError(err)