Skip to content

Commit b299658

Browse files
authored
feat: add MCP server card (SEP-1649) (#453)
Add getServerCard() function that returns structured server metadata for client discovery via /.well-known/mcp/server-card.json. - Reuse existing constants (SERVER_NAME, SERVER_VERSION, APIFY_MCP_URL) - Load description dynamically from server.json via readJsonFile helper - Export ServerCard type and getServerCard via index-internals - Add readJsonFile utility, fix createRequire anti-pattern in version.ts - Export APIFY_FAVICON_URL and SERVER_TITLE for internal repo reuse
1 parent a772ba4 commit b299658

7 files changed

Lines changed: 161 additions & 6 deletions

File tree

src/const.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ export const TOOL_MAX_OUTPUT_CHARS = 50000;
1818

1919
// MCP Server
2020
export const SERVER_NAME = 'apify-mcp-server';
21+
export const SERVER_TITLE = 'Apify MCP Server';
2122
export const SERVER_VERSION = '1.0.0';
2223

2324
// User agent headers
@@ -171,7 +172,9 @@ export const ALLOWED_DOC_DOMAINS = [
171172
export const PROGRESS_NOTIFICATION_INTERVAL_MS = 5_000; // 5 seconds
172173

173174
export const APIFY_STORE_URL = 'https://apify.com';
175+
export const APIFY_FAVICON_URL = `${APIFY_STORE_URL}/favicon.ico`;
174176
export const APIFY_MCP_URL = 'https://mcp.apify.com';
177+
export const APIFY_DOCS_MCP_URL = 'https://docs.apify.com/platform/integrations/mcp';
175178

176179
// Telemetry
177180
export const TELEMETRY_ENV = {

src/index-internals.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,38 +3,45 @@
33
*/
44

55
import { ApifyClient } from './apify-client.js';
6-
import { defaults, HelperTools } from './const.js';
6+
import { APIFY_FAVICON_URL, defaults, HelperTools, SERVER_NAME, SERVER_TITLE } from './const.js';
77
import { processParamsGetTools } from './mcp/utils.js';
8+
import { getServerCard } from './server_card.js';
89
import { addTool } from './tools/helpers.js';
910
import { defaultTools, getActorsAsTools, getUnauthEnabledToolCategories, toolCategories,
1011
toolCategoriesEnabledByDefault, unauthEnabledTools } from './tools/index.js';
1112
import { actorNameToToolName } from './tools/utils.js';
12-
import type { ActorStore, ToolCategory, UiMode } from './types.js';
13-
import { parseCommaSeparatedList, parseQueryParamList } from './utils/generic.js';
13+
import type { ActorStore, ServerCard, ToolCategory, UiMode } from './types.js';
14+
import { parseCommaSeparatedList, parseQueryParamList, readJsonFile } from './utils/generic.js';
1415
import { redactSkyfirePayId } from './utils/logging.js';
1516
import { getExpectedToolNamesByCategories } from './utils/tool-categories-helpers.js';
1617
import { getToolPublicFieldOnly } from './utils/tools.js';
1718
import { TTLLRUCache } from './utils/ttl-lru.js';
1819

1920
export {
21+
APIFY_FAVICON_URL,
2022
ApifyClient,
2123
getExpectedToolNamesByCategories,
24+
getServerCard,
2225
TTLLRUCache,
2326
actorNameToToolName,
2427
HelperTools,
28+
SERVER_NAME,
29+
SERVER_TITLE,
2530
defaults,
2631
defaultTools,
2732
addTool,
2833
toolCategories,
2934
toolCategoriesEnabledByDefault,
3035
type ActorStore,
36+
type ServerCard,
3137
type ToolCategory,
3238
type UiMode,
3339
processParamsGetTools,
3440
getActorsAsTools,
3541
getToolPublicFieldOnly,
3642
getUnauthEnabledToolCategories,
3743
unauthEnabledTools,
44+
readJsonFile,
3845
parseCommaSeparatedList,
3946
parseQueryParamList,
4047
redactSkyfirePayId,

src/server_card.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { LATEST_PROTOCOL_VERSION } from '@modelcontextprotocol/sdk/types.js';
2+
3+
import { APIFY_DOCS_MCP_URL, APIFY_FAVICON_URL, SERVER_NAME, SERVER_TITLE, SERVER_VERSION } from './const.js';
4+
import type { ServerCard } from './types.js';
5+
import { readJsonFile } from './utils/generic.js';
6+
7+
const serverJson = readJsonFile<{ description: string }>(import.meta.url, '../server.json');
8+
9+
/** Returns the MCP server card object per SEP-1649. */
10+
export function getServerCard(): ServerCard {
11+
return {
12+
$schema: 'https://static.modelcontextprotocol.io/schemas/mcp-server-card/v1.json',
13+
version: '1.0',
14+
protocolVersion: LATEST_PROTOCOL_VERSION,
15+
serverInfo: {
16+
name: SERVER_NAME,
17+
title: SERVER_TITLE,
18+
version: SERVER_VERSION,
19+
},
20+
description: serverJson.description,
21+
iconUrl: APIFY_FAVICON_URL,
22+
documentationUrl: APIFY_DOCS_MCP_URL,
23+
transport: {
24+
type: 'streamable-http',
25+
endpoint: '/',
26+
},
27+
capabilities: {
28+
tools: { listChanged: true },
29+
},
30+
authentication: {
31+
required: true,
32+
schemes: ['bearer', 'oauth2'],
33+
},
34+
tools: 'dynamic',
35+
};
36+
}

src/types.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -512,3 +512,30 @@ export type ApifyRequestParams = {
512512
/** Allow any other request parameters */
513513
[key: string]: unknown;
514514
};
515+
516+
/** MCP Server Card per SEP-1649. */
517+
export type ServerCard = {
518+
$schema: string;
519+
version: string;
520+
protocolVersion: string;
521+
serverInfo: {
522+
name: string;
523+
title: string;
524+
version: string;
525+
};
526+
description: string;
527+
iconUrl: string;
528+
documentationUrl: string;
529+
transport: {
530+
type: string;
531+
endpoint: string;
532+
};
533+
capabilities: {
534+
tools: { listChanged: boolean };
535+
};
536+
authentication: {
537+
required: boolean;
538+
schemes: string[];
539+
};
540+
tools: string;
541+
};

src/utils/generic.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,22 @@
1+
import { readFileSync } from 'node:fs';
2+
import { dirname, resolve } from 'node:path';
3+
import { fileURLToPath } from 'node:url';
4+
5+
/**
6+
* Reads and parses a JSON file relative to the caller's module URL.
7+
* Resolves the path from the directory of the calling module (via `import.meta.url`).
8+
*
9+
* @param importMetaUrl - The `import.meta.url` of the calling module.
10+
* @param relativePath - The relative path to the JSON file from the calling module.
11+
* @returns The parsed JSON content.
12+
* @example
13+
* const serverJson = readJsonFile(import.meta.url, '../../server.json');
14+
*/
15+
export function readJsonFile<T = unknown>(importMetaUrl: string, relativePath: string): T {
16+
const jsonPath = resolve(dirname(fileURLToPath(importMetaUrl)), relativePath);
17+
return JSON.parse(readFileSync(jsonPath, 'utf-8')) as T;
18+
}
19+
120
/**
221
* Parses a comma-separated string into an array of trimmed strings.
322
* Empty strings are filtered out after trimming.

src/utils/version.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
1-
import { createRequire } from 'node:module';
1+
import { readJsonFile } from './generic.js';
22

3-
const require = createRequire(import.meta.url);
4-
const packageJson = require('../../package.json');
3+
const packageJson = readJsonFile<{ version?: string }>(import.meta.url, '../../package.json');
54

65
/**
76
* Gets the package version from package.json

tests/unit/server-card.test.ts

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import { LATEST_PROTOCOL_VERSION } from '@modelcontextprotocol/sdk/types.js';
2+
import { describe, expect, it } from 'vitest';
3+
4+
import { SERVER_NAME, SERVER_TITLE, SERVER_VERSION } from '../../src/const.js';
5+
import { getServerCard } from '../../src/server_card.js';
6+
import { readJsonFile } from '../../src/utils/generic.js';
7+
8+
const serverJson = readJsonFile<{ description: string }>(import.meta.url, '../../server.json');
9+
10+
describe('getServerCard', () => {
11+
it('should return a valid MCP server card object', () => {
12+
const card = getServerCard();
13+
14+
expect(card.$schema).toBe('https://static.modelcontextprotocol.io/schemas/mcp-server-card/v1.json');
15+
expect(card.version).toBe('1.0');
16+
expect(card.protocolVersion).toBe(LATEST_PROTOCOL_VERSION);
17+
});
18+
19+
it('should contain required serverInfo fields using constants from const.ts', () => {
20+
const card = getServerCard();
21+
22+
expect(card.serverInfo.name).toBe(SERVER_NAME);
23+
expect(card.serverInfo.title).toBe(SERVER_TITLE);
24+
expect(card.serverInfo.version).toBe(SERVER_VERSION);
25+
});
26+
27+
it('should declare streamable-http transport at root endpoint', () => {
28+
const card = getServerCard();
29+
30+
expect(card.transport.type).toBe('streamable-http');
31+
expect(card.transport.endpoint).toBe('/');
32+
});
33+
34+
it('should declare tools capability with listChanged', () => {
35+
const card = getServerCard();
36+
37+
expect(card.capabilities.tools.listChanged).toBe(true);
38+
});
39+
40+
it('should require authentication with bearer and oauth2 schemes', () => {
41+
const card = getServerCard();
42+
43+
expect(card.authentication.required).toBe(true);
44+
expect(card.authentication.schemes).toEqual(['bearer', 'oauth2']);
45+
});
46+
47+
it('should declare tools as dynamic', () => {
48+
const card = getServerCard();
49+
50+
expect(card.tools).toBe('dynamic');
51+
});
52+
53+
it('should load description from server.json', () => {
54+
const card = getServerCard();
55+
56+
expect(card.description).toBe(serverJson.description);
57+
});
58+
59+
it('should include documentation URL', () => {
60+
const card = getServerCard();
61+
62+
expect(card.documentationUrl).toBe('https://docs.apify.com/platform/integrations/mcp');
63+
});
64+
});

0 commit comments

Comments
 (0)