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
28 changes: 26 additions & 2 deletions src/mcp-servers/datasets/tools.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,22 @@ 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<string, (url: URL) => any> = {}
const routes: Record<string, (url: URL, req: IncomingMessage) => any> = {}

const fakeApi = createServer((req: IncomingMessage, res: ServerResponse) => {
const url = new URL(req.url!, 'http://localhost')
// Sort patterns by length descending so more specific routes match first
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
Expand Down Expand Up @@ -72,6 +74,28 @@ after(async () => {
await new Promise<void>(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<string, string>
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<string, string>
assert.equal(headers.Referer, undefined)
})
})

describe('list_datasets', () => {
it('should list datasets and return formatted results', async () => {
routes['/catalog/datasets'] = (url) => ({
Expand Down
5 changes: 3 additions & 2 deletions src/mcp-servers/datasets/tools/_utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, string> = {
'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
}
Expand Down
2 changes: 1 addition & 1 deletion src/mcp-servers/datasets/tools/aggregate-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion src/mcp-servers/datasets/tools/calculate-metric.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
4 changes: 2 additions & 2 deletions src/mcp-servers/datasets/tools/describe-dataset.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion src/mcp-servers/datasets/tools/get-field-values.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion src/mcp-servers/datasets/tools/search-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion src/mcp-servers/datasets/tools/search-datasets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Loading