pulsemcp-cms-admin-mcp-server 0.3.0 → 0.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/shared/src/pulsemcp-admin-client/lib/get-provider-by-id.js +24 -0
- package/build/shared/src/pulsemcp-admin-client/lib/save-mcp-implementation.js +16 -14
- package/build/shared/src/pulsemcp-admin-client/lib/search-providers.js +45 -0
- package/build/shared/src/pulsemcp-admin-client/pulsemcp-admin-client.integration-mock.js +63 -0
- package/build/shared/src/server.js +8 -0
- package/build/shared/src/tools/find-providers.js +160 -0
- package/build/shared/src/tools/search-mcp-implementations.js +37 -0
- package/build/shared/src/tools.js +2 -0
- package/package.json +1 -1
- package/shared/pulsemcp-admin-client/lib/get-provider-by-id.d.ts +3 -0
- package/shared/pulsemcp-admin-client/lib/get-provider-by-id.js +24 -0
- package/shared/pulsemcp-admin-client/lib/save-mcp-implementation.js +16 -14
- package/shared/pulsemcp-admin-client/lib/search-providers.d.ts +7 -0
- package/shared/pulsemcp-admin-client/lib/search-providers.js +45 -0
- package/shared/pulsemcp-admin-client/pulsemcp-admin-client.integration-mock.d.ts +5 -1
- package/shared/pulsemcp-admin-client/pulsemcp-admin-client.integration-mock.js +63 -0
- package/shared/server.d.ts +13 -1
- package/shared/server.js +8 -0
- package/shared/tools/find-providers.d.ts +41 -0
- package/shared/tools/find-providers.js +160 -0
- package/shared/tools/search-mcp-implementations.js +37 -0
- package/shared/tools.js +2 -0
- package/shared/types.d.ts +20 -0
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export async function getProviderById(apiKey, baseUrl, id) {
|
|
2
|
+
const url = new URL(`/api/providers/${id}`, baseUrl);
|
|
3
|
+
const response = await fetch(url.toString(), {
|
|
4
|
+
method: 'GET',
|
|
5
|
+
headers: {
|
|
6
|
+
'X-API-Key': apiKey,
|
|
7
|
+
Accept: 'application/json',
|
|
8
|
+
},
|
|
9
|
+
});
|
|
10
|
+
if (response.status === 404) {
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
if (!response.ok) {
|
|
14
|
+
if (response.status === 401) {
|
|
15
|
+
throw new Error('Invalid API key');
|
|
16
|
+
}
|
|
17
|
+
if (response.status === 403) {
|
|
18
|
+
throw new Error('User lacks admin privileges');
|
|
19
|
+
}
|
|
20
|
+
throw new Error(`Failed to fetch provider: ${response.status} ${response.statusText}`);
|
|
21
|
+
}
|
|
22
|
+
const data = (await response.json());
|
|
23
|
+
return data;
|
|
24
|
+
}
|
|
@@ -67,50 +67,52 @@ export async function saveMCPImplementation(apiKey, baseUrl, id, params) {
|
|
|
67
67
|
formData.append('mcp_implementation[internal_notes]', params.internal_notes);
|
|
68
68
|
}
|
|
69
69
|
// Remote endpoints
|
|
70
|
+
// Rails expects nested attributes to use the _attributes suffix for has_many associations
|
|
70
71
|
if (params.remote !== undefined && params.remote.length > 0) {
|
|
71
72
|
params.remote.forEach((remote, index) => {
|
|
72
73
|
if (remote.id !== undefined) {
|
|
73
|
-
formData.append(`mcp_implementation[
|
|
74
|
+
formData.append(`mcp_implementation[remote_attributes][${index}][id]`, remote.id.toString());
|
|
74
75
|
}
|
|
75
76
|
if (remote.url_direct !== undefined) {
|
|
76
|
-
formData.append(`mcp_implementation[
|
|
77
|
+
formData.append(`mcp_implementation[remote_attributes][${index}][url_direct]`, remote.url_direct);
|
|
77
78
|
}
|
|
78
79
|
if (remote.url_setup !== undefined) {
|
|
79
|
-
formData.append(`mcp_implementation[
|
|
80
|
+
formData.append(`mcp_implementation[remote_attributes][${index}][url_setup]`, remote.url_setup);
|
|
80
81
|
}
|
|
81
82
|
if (remote.transport !== undefined) {
|
|
82
|
-
formData.append(`mcp_implementation[
|
|
83
|
+
formData.append(`mcp_implementation[remote_attributes][${index}][transport]`, remote.transport);
|
|
83
84
|
}
|
|
84
85
|
if (remote.host_platform !== undefined) {
|
|
85
|
-
formData.append(`mcp_implementation[
|
|
86
|
+
formData.append(`mcp_implementation[remote_attributes][${index}][host_platform]`, remote.host_platform);
|
|
86
87
|
}
|
|
87
88
|
if (remote.host_infrastructure !== undefined) {
|
|
88
|
-
formData.append(`mcp_implementation[
|
|
89
|
+
formData.append(`mcp_implementation[remote_attributes][${index}][host_infrastructure]`, remote.host_infrastructure);
|
|
89
90
|
}
|
|
90
91
|
if (remote.authentication_method !== undefined) {
|
|
91
|
-
formData.append(`mcp_implementation[
|
|
92
|
+
formData.append(`mcp_implementation[remote_attributes][${index}][authentication_method]`, remote.authentication_method);
|
|
92
93
|
}
|
|
93
94
|
if (remote.cost !== undefined) {
|
|
94
|
-
formData.append(`mcp_implementation[
|
|
95
|
+
formData.append(`mcp_implementation[remote_attributes][${index}][cost]`, remote.cost);
|
|
95
96
|
}
|
|
96
97
|
if (remote.status !== undefined) {
|
|
97
|
-
formData.append(`mcp_implementation[
|
|
98
|
+
formData.append(`mcp_implementation[remote_attributes][${index}][status]`, remote.status);
|
|
98
99
|
}
|
|
99
100
|
if (remote.display_name !== undefined) {
|
|
100
|
-
formData.append(`mcp_implementation[
|
|
101
|
+
formData.append(`mcp_implementation[remote_attributes][${index}][display_name]`, remote.display_name);
|
|
101
102
|
}
|
|
102
103
|
if (remote.internal_notes !== undefined) {
|
|
103
|
-
formData.append(`mcp_implementation[
|
|
104
|
+
formData.append(`mcp_implementation[remote_attributes][${index}][internal_notes]`, remote.internal_notes);
|
|
104
105
|
}
|
|
105
106
|
});
|
|
106
107
|
}
|
|
107
108
|
// Canonical URLs
|
|
109
|
+
// Rails expects nested attributes to use the _attributes suffix for has_many associations
|
|
108
110
|
if (params.canonical !== undefined && params.canonical.length > 0) {
|
|
109
111
|
params.canonical.forEach((canonicalUrl, index) => {
|
|
110
|
-
formData.append(`mcp_implementation[
|
|
111
|
-
formData.append(`mcp_implementation[
|
|
112
|
+
formData.append(`mcp_implementation[canonical_attributes][${index}][url]`, canonicalUrl.url);
|
|
113
|
+
formData.append(`mcp_implementation[canonical_attributes][${index}][scope]`, canonicalUrl.scope);
|
|
112
114
|
if (canonicalUrl.note !== undefined) {
|
|
113
|
-
formData.append(`mcp_implementation[
|
|
115
|
+
formData.append(`mcp_implementation[canonical_attributes][${index}][note]`, canonicalUrl.note);
|
|
114
116
|
}
|
|
115
117
|
});
|
|
116
118
|
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
export async function searchProviders(apiKey, baseUrl, params) {
|
|
2
|
+
// Endpoint implemented at: /api/providers/search
|
|
3
|
+
// Requires admin authentication via X-API-Key header
|
|
4
|
+
const url = new URL('/api/providers/search', baseUrl);
|
|
5
|
+
// Add query parameters
|
|
6
|
+
url.searchParams.append('q', params.query);
|
|
7
|
+
if (params.limit) {
|
|
8
|
+
url.searchParams.append('limit', params.limit.toString());
|
|
9
|
+
}
|
|
10
|
+
if (params.offset) {
|
|
11
|
+
url.searchParams.append('offset', params.offset.toString());
|
|
12
|
+
}
|
|
13
|
+
const response = await fetch(url.toString(), {
|
|
14
|
+
method: 'GET',
|
|
15
|
+
headers: {
|
|
16
|
+
'X-API-Key': apiKey,
|
|
17
|
+
Accept: 'application/json',
|
|
18
|
+
},
|
|
19
|
+
});
|
|
20
|
+
if (!response.ok) {
|
|
21
|
+
if (response.status === 401) {
|
|
22
|
+
throw new Error('Invalid API key');
|
|
23
|
+
}
|
|
24
|
+
if (response.status === 403) {
|
|
25
|
+
throw new Error('User lacks admin privileges');
|
|
26
|
+
}
|
|
27
|
+
if (response.status === 404) {
|
|
28
|
+
throw new Error('Providers search endpoint not found');
|
|
29
|
+
}
|
|
30
|
+
throw new Error(`Failed to search providers: ${response.status} ${response.statusText}`);
|
|
31
|
+
}
|
|
32
|
+
const data = (await response.json());
|
|
33
|
+
return {
|
|
34
|
+
providers: data.data || [],
|
|
35
|
+
pagination: data.meta
|
|
36
|
+
? {
|
|
37
|
+
current_page: data.meta.current_page,
|
|
38
|
+
total_pages: data.meta.total_pages,
|
|
39
|
+
total_count: data.meta.total_count,
|
|
40
|
+
has_next: data.meta.has_next,
|
|
41
|
+
limit: data.meta.limit,
|
|
42
|
+
}
|
|
43
|
+
: undefined,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
@@ -334,5 +334,68 @@ export function createMockPulseMCPAdminClient(mockData) {
|
|
|
334
334
|
updated_at: new Date().toISOString(),
|
|
335
335
|
};
|
|
336
336
|
},
|
|
337
|
+
async searchProviders(params) {
|
|
338
|
+
if (mockData.errors?.searchProviders) {
|
|
339
|
+
throw mockData.errors.searchProviders;
|
|
340
|
+
}
|
|
341
|
+
if (mockData.providersResponse) {
|
|
342
|
+
return mockData.providersResponse;
|
|
343
|
+
}
|
|
344
|
+
let providers = mockData.providers || [
|
|
345
|
+
{
|
|
346
|
+
id: 1,
|
|
347
|
+
name: 'Test Provider',
|
|
348
|
+
slug: 'test-provider',
|
|
349
|
+
url: 'https://testprovider.com',
|
|
350
|
+
implementations_count: 5,
|
|
351
|
+
created_at: '2024-01-01T00:00:00Z',
|
|
352
|
+
updated_at: '2024-01-01T00:00:00Z',
|
|
353
|
+
},
|
|
354
|
+
];
|
|
355
|
+
// Apply search filter
|
|
356
|
+
if (params.query) {
|
|
357
|
+
const query = params.query.toLowerCase();
|
|
358
|
+
providers = providers.filter((provider) => provider.name.toLowerCase().includes(query) ||
|
|
359
|
+
provider.slug.toLowerCase().includes(query) ||
|
|
360
|
+
(provider.url && provider.url.toLowerCase().includes(query)));
|
|
361
|
+
}
|
|
362
|
+
// Apply pagination
|
|
363
|
+
const offset = params.offset || 0;
|
|
364
|
+
const limit = params.limit || 30;
|
|
365
|
+
const totalCount = providers.length;
|
|
366
|
+
const paginatedProviders = providers.slice(offset, offset + limit);
|
|
367
|
+
return {
|
|
368
|
+
providers: paginatedProviders,
|
|
369
|
+
pagination: {
|
|
370
|
+
current_page: Math.floor(offset / limit) + 1,
|
|
371
|
+
total_pages: Math.ceil(totalCount / limit),
|
|
372
|
+
total_count: totalCount,
|
|
373
|
+
has_next: offset + limit < totalCount,
|
|
374
|
+
limit: limit,
|
|
375
|
+
},
|
|
376
|
+
};
|
|
377
|
+
},
|
|
378
|
+
async getProviderById(id) {
|
|
379
|
+
if (mockData.errors?.getProviderById) {
|
|
380
|
+
throw mockData.errors.getProviderById;
|
|
381
|
+
}
|
|
382
|
+
// Find provider in mock data by ID
|
|
383
|
+
const providers = mockData.providers || [
|
|
384
|
+
{
|
|
385
|
+
id: 1,
|
|
386
|
+
name: 'Test Provider',
|
|
387
|
+
slug: 'test-provider',
|
|
388
|
+
url: 'https://testprovider.com',
|
|
389
|
+
implementations_count: 5,
|
|
390
|
+
created_at: '2024-01-01T00:00:00Z',
|
|
391
|
+
updated_at: '2024-01-01T00:00:00Z',
|
|
392
|
+
},
|
|
393
|
+
];
|
|
394
|
+
const found = providers.find((p) => p.id === id);
|
|
395
|
+
if (found)
|
|
396
|
+
return found;
|
|
397
|
+
// Return null if not found
|
|
398
|
+
return null;
|
|
399
|
+
},
|
|
337
400
|
};
|
|
338
401
|
}
|
|
@@ -75,6 +75,14 @@ export class PulseMCPAdminClient {
|
|
|
75
75
|
const { sendEmail } = await import('./pulsemcp-admin-client/lib/send-email.js');
|
|
76
76
|
return sendEmail(this.apiKey, this.baseUrl, params);
|
|
77
77
|
}
|
|
78
|
+
async searchProviders(params) {
|
|
79
|
+
const { searchProviders } = await import('./pulsemcp-admin-client/lib/search-providers.js');
|
|
80
|
+
return searchProviders(this.apiKey, this.baseUrl, params);
|
|
81
|
+
}
|
|
82
|
+
async getProviderById(id) {
|
|
83
|
+
const { getProviderById } = await import('./pulsemcp-admin-client/lib/get-provider-by-id.js');
|
|
84
|
+
return getProviderById(this.apiKey, this.baseUrl, id);
|
|
85
|
+
}
|
|
78
86
|
}
|
|
79
87
|
export function createMCPServer() {
|
|
80
88
|
const server = new Server({
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
// Parameter descriptions - single source of truth
|
|
3
|
+
const PARAM_DESCRIPTIONS = {
|
|
4
|
+
id: 'Provider ID to retrieve a specific provider',
|
|
5
|
+
query: 'Search query to find providers by name, URL, or slug',
|
|
6
|
+
limit: 'Maximum number of results to return (1-100, default: 30)',
|
|
7
|
+
offset: 'Number of results to skip for pagination (default: 0)',
|
|
8
|
+
};
|
|
9
|
+
const FindProvidersSchema = z
|
|
10
|
+
.object({
|
|
11
|
+
id: z.number().int().positive().optional().describe(PARAM_DESCRIPTIONS.id),
|
|
12
|
+
query: z.string().min(1).optional().describe(PARAM_DESCRIPTIONS.query),
|
|
13
|
+
limit: z.number().min(1).max(100).optional().describe(PARAM_DESCRIPTIONS.limit),
|
|
14
|
+
offset: z.number().min(0).optional().describe(PARAM_DESCRIPTIONS.offset),
|
|
15
|
+
})
|
|
16
|
+
.refine((data) => data.id !== undefined || data.query !== undefined, {
|
|
17
|
+
message: 'Either id or query must be provided',
|
|
18
|
+
});
|
|
19
|
+
export function findProviders(_server, clientFactory) {
|
|
20
|
+
return {
|
|
21
|
+
name: 'find_providers',
|
|
22
|
+
description: `Find providers (organizations/individuals) in the PulseMCP registry.
|
|
23
|
+
|
|
24
|
+
This tool can operate in two modes:
|
|
25
|
+
|
|
26
|
+
1. **Find by ID**: Retrieve a specific provider by its numeric ID
|
|
27
|
+
- Returns a single provider with detailed information
|
|
28
|
+
- Returns null if not found
|
|
29
|
+
|
|
30
|
+
2. **Search by query**: Search for providers by name, URL, or slug
|
|
31
|
+
- Searches across provider name, URL, and slug fields (case-insensitive)
|
|
32
|
+
- Returns a list of matching providers with pagination support
|
|
33
|
+
- Each result includes implementation counts
|
|
34
|
+
|
|
35
|
+
Provider information includes:
|
|
36
|
+
- ID and slug
|
|
37
|
+
- Name and URL
|
|
38
|
+
- Number of associated implementations
|
|
39
|
+
- Creation and update timestamps
|
|
40
|
+
|
|
41
|
+
Use cases:
|
|
42
|
+
- Look up a specific provider by ID
|
|
43
|
+
- Search for providers by name (e.g., "anthropic", "modelcontextprotocol")
|
|
44
|
+
- Find providers by partial name matches
|
|
45
|
+
- Discover all providers for an organization
|
|
46
|
+
- Check how many implementations a provider has published
|
|
47
|
+
|
|
48
|
+
Note: This tool queries the PulseMCP registry API. Results depend on what has been published to the registry.`,
|
|
49
|
+
inputSchema: {
|
|
50
|
+
type: 'object',
|
|
51
|
+
properties: {
|
|
52
|
+
id: {
|
|
53
|
+
type: 'number',
|
|
54
|
+
description: PARAM_DESCRIPTIONS.id,
|
|
55
|
+
},
|
|
56
|
+
query: {
|
|
57
|
+
type: 'string',
|
|
58
|
+
description: PARAM_DESCRIPTIONS.query,
|
|
59
|
+
},
|
|
60
|
+
limit: {
|
|
61
|
+
type: 'number',
|
|
62
|
+
description: PARAM_DESCRIPTIONS.limit,
|
|
63
|
+
},
|
|
64
|
+
offset: {
|
|
65
|
+
type: 'number',
|
|
66
|
+
description: PARAM_DESCRIPTIONS.offset,
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
handler: async (args) => {
|
|
71
|
+
const validatedArgs = FindProvidersSchema.parse(args);
|
|
72
|
+
const client = clientFactory();
|
|
73
|
+
try {
|
|
74
|
+
// Mode 1: Find by ID
|
|
75
|
+
if (validatedArgs.id !== undefined) {
|
|
76
|
+
const provider = await client.getProviderById(validatedArgs.id);
|
|
77
|
+
if (!provider) {
|
|
78
|
+
return {
|
|
79
|
+
content: [
|
|
80
|
+
{
|
|
81
|
+
type: 'text',
|
|
82
|
+
text: `Provider with ID ${validatedArgs.id} not found.`,
|
|
83
|
+
},
|
|
84
|
+
],
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
let content = `**${provider.name}**\n`;
|
|
88
|
+
content += `ID: ${provider.id}\n`;
|
|
89
|
+
content += `Slug: ${provider.slug}\n`;
|
|
90
|
+
if (provider.url) {
|
|
91
|
+
content += `URL: ${provider.url}\n`;
|
|
92
|
+
}
|
|
93
|
+
if (provider.implementations_count !== undefined) {
|
|
94
|
+
content += `Implementations: ${provider.implementations_count}\n`;
|
|
95
|
+
}
|
|
96
|
+
if (provider.created_at) {
|
|
97
|
+
content += `Created: ${provider.created_at}\n`;
|
|
98
|
+
}
|
|
99
|
+
return {
|
|
100
|
+
content: [
|
|
101
|
+
{
|
|
102
|
+
type: 'text',
|
|
103
|
+
text: content.trim(),
|
|
104
|
+
},
|
|
105
|
+
],
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
// Mode 2: Search by query
|
|
109
|
+
if (validatedArgs.query !== undefined) {
|
|
110
|
+
const response = await client.searchProviders({
|
|
111
|
+
query: validatedArgs.query,
|
|
112
|
+
limit: validatedArgs.limit,
|
|
113
|
+
offset: validatedArgs.offset,
|
|
114
|
+
});
|
|
115
|
+
let content = `Found ${response.providers.length} provider(s) matching "${validatedArgs.query}"`;
|
|
116
|
+
if (response.pagination) {
|
|
117
|
+
content += ` (showing ${response.providers.length} of ${response.pagination.total_count} total)`;
|
|
118
|
+
}
|
|
119
|
+
content += ':\n\n';
|
|
120
|
+
for (const [index, provider] of response.providers.entries()) {
|
|
121
|
+
content += `${index + 1}. **${provider.name}**\n`;
|
|
122
|
+
content += ` ID: ${provider.id} | Slug: ${provider.slug}\n`;
|
|
123
|
+
if (provider.url) {
|
|
124
|
+
content += ` URL: ${provider.url}\n`;
|
|
125
|
+
}
|
|
126
|
+
if (provider.implementations_count !== undefined) {
|
|
127
|
+
content += ` Implementations: ${provider.implementations_count}\n`;
|
|
128
|
+
}
|
|
129
|
+
content += '\n';
|
|
130
|
+
}
|
|
131
|
+
if (response.pagination?.has_next) {
|
|
132
|
+
const nextOffset = (validatedArgs.offset || 0) + (validatedArgs.limit || 30);
|
|
133
|
+
content += `\n---\nMore results available. Use offset=${nextOffset} to see the next page.`;
|
|
134
|
+
}
|
|
135
|
+
return {
|
|
136
|
+
content: [
|
|
137
|
+
{
|
|
138
|
+
type: 'text',
|
|
139
|
+
text: content.trim(),
|
|
140
|
+
},
|
|
141
|
+
],
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
// This should never happen due to zod refinement, but TypeScript needs it
|
|
145
|
+
throw new Error('Either id or query must be provided');
|
|
146
|
+
}
|
|
147
|
+
catch (error) {
|
|
148
|
+
return {
|
|
149
|
+
content: [
|
|
150
|
+
{
|
|
151
|
+
type: 'text',
|
|
152
|
+
text: `Error finding providers: ${error instanceof Error ? error.message : String(error)}`,
|
|
153
|
+
},
|
|
154
|
+
],
|
|
155
|
+
isError: true,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
},
|
|
159
|
+
};
|
|
160
|
+
}
|
|
@@ -38,6 +38,8 @@ Returns a list of matching implementations with their metadata, including:
|
|
|
38
38
|
- GitHub stars and popularity metrics
|
|
39
39
|
- Associated MCP server/client IDs
|
|
40
40
|
- PulseMCP web URL
|
|
41
|
+
- Remote endpoints (for servers) - hosting platforms, transport methods, authentication
|
|
42
|
+
- Canonical URLs - authoritative sources for the implementation
|
|
41
43
|
|
|
42
44
|
Use cases:
|
|
43
45
|
- Find existing MCP servers before creating a new one
|
|
@@ -96,6 +98,7 @@ Note: This tool queries the PulseMCP registry API. Results depend on what has be
|
|
|
96
98
|
content += ':\n\n';
|
|
97
99
|
for (const [index, impl] of response.implementations.entries()) {
|
|
98
100
|
content += `${index + 1}. **${impl.name}** (${impl.type})\n`;
|
|
101
|
+
content += ` ID: ${impl.id}\n`;
|
|
99
102
|
content += ` Slug: ${impl.slug}\n`;
|
|
100
103
|
content += ` Status: ${impl.status}`;
|
|
101
104
|
if (impl.classification) {
|
|
@@ -124,6 +127,40 @@ Note: This tool queries the PulseMCP registry API. Results depend on what has be
|
|
|
124
127
|
if (impl.mcp_client_id) {
|
|
125
128
|
content += ` MCP Client ID: ${impl.mcp_client_id}\n`;
|
|
126
129
|
}
|
|
130
|
+
// Display remote endpoints if available
|
|
131
|
+
if (impl.mcp_server?.remotes && impl.mcp_server.remotes.length > 0) {
|
|
132
|
+
content += ` Remotes (${impl.mcp_server.remotes.length}):\n`;
|
|
133
|
+
for (const remote of impl.mcp_server.remotes) {
|
|
134
|
+
let remoteLine = ` - ${remote.display_name || remote.url_direct || remote.url_setup || 'Unnamed'}`;
|
|
135
|
+
const attrs = [];
|
|
136
|
+
if (remote.transport)
|
|
137
|
+
attrs.push(remote.transport);
|
|
138
|
+
if (remote.host_platform)
|
|
139
|
+
attrs.push(remote.host_platform);
|
|
140
|
+
if (remote.authentication_method)
|
|
141
|
+
attrs.push(`auth: ${remote.authentication_method}`);
|
|
142
|
+
if (remote.cost)
|
|
143
|
+
attrs.push(remote.cost);
|
|
144
|
+
if (attrs.length > 0) {
|
|
145
|
+
remoteLine += ` [${attrs.join(', ')}]`;
|
|
146
|
+
}
|
|
147
|
+
content += remoteLine + '\n';
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
else if (impl.mcp_server?.mcp_server_remotes_count) {
|
|
151
|
+
content += ` Remotes Count: ${impl.mcp_server.mcp_server_remotes_count}\n`;
|
|
152
|
+
}
|
|
153
|
+
// Display canonical URLs if available
|
|
154
|
+
if (impl.canonical && impl.canonical.length > 0) {
|
|
155
|
+
content += ` Canonical URLs (${impl.canonical.length}):\n`;
|
|
156
|
+
for (const canonical of impl.canonical) {
|
|
157
|
+
let canonicalLine = ` - ${canonical.url} [scope: ${canonical.scope}]`;
|
|
158
|
+
if (canonical.note) {
|
|
159
|
+
canonicalLine += ` - ${canonical.note}`;
|
|
160
|
+
}
|
|
161
|
+
content += canonicalLine + '\n';
|
|
162
|
+
}
|
|
163
|
+
}
|
|
127
164
|
content += '\n';
|
|
128
165
|
}
|
|
129
166
|
if (response.pagination?.has_next) {
|
|
@@ -9,6 +9,7 @@ import { searchMCPImplementations } from './tools/search-mcp-implementations.js'
|
|
|
9
9
|
import { getDraftMCPImplementations } from './tools/get-draft-mcp-implementations.js';
|
|
10
10
|
import { saveMCPImplementation } from './tools/save-mcp-implementation.js';
|
|
11
11
|
import { sendMCPImplementationPostingNotification } from './tools/send-mcp-implementation-posting-notification.js';
|
|
12
|
+
import { findProviders } from './tools/find-providers.js';
|
|
12
13
|
const ALL_TOOLS = [
|
|
13
14
|
{ factory: getNewsletterPosts, groups: ['newsletter'] },
|
|
14
15
|
{ factory: getNewsletterPost, groups: ['newsletter'] },
|
|
@@ -20,6 +21,7 @@ const ALL_TOOLS = [
|
|
|
20
21
|
{ factory: getDraftMCPImplementations, groups: ['server_queue_readonly', 'server_queue_all'] },
|
|
21
22
|
{ factory: saveMCPImplementation, groups: ['server_queue_all'] },
|
|
22
23
|
{ factory: sendMCPImplementationPostingNotification, groups: ['server_queue_all'] },
|
|
24
|
+
{ factory: findProviders, groups: ['server_queue_readonly', 'server_queue_all'] },
|
|
23
25
|
];
|
|
24
26
|
/**
|
|
25
27
|
* Parse enabled tool groups from environment variable or parameter
|
package/package.json
CHANGED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export async function getProviderById(apiKey, baseUrl, id) {
|
|
2
|
+
const url = new URL(`/api/providers/${id}`, baseUrl);
|
|
3
|
+
const response = await fetch(url.toString(), {
|
|
4
|
+
method: 'GET',
|
|
5
|
+
headers: {
|
|
6
|
+
'X-API-Key': apiKey,
|
|
7
|
+
Accept: 'application/json',
|
|
8
|
+
},
|
|
9
|
+
});
|
|
10
|
+
if (response.status === 404) {
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
if (!response.ok) {
|
|
14
|
+
if (response.status === 401) {
|
|
15
|
+
throw new Error('Invalid API key');
|
|
16
|
+
}
|
|
17
|
+
if (response.status === 403) {
|
|
18
|
+
throw new Error('User lacks admin privileges');
|
|
19
|
+
}
|
|
20
|
+
throw new Error(`Failed to fetch provider: ${response.status} ${response.statusText}`);
|
|
21
|
+
}
|
|
22
|
+
const data = (await response.json());
|
|
23
|
+
return data;
|
|
24
|
+
}
|
|
@@ -67,50 +67,52 @@ export async function saveMCPImplementation(apiKey, baseUrl, id, params) {
|
|
|
67
67
|
formData.append('mcp_implementation[internal_notes]', params.internal_notes);
|
|
68
68
|
}
|
|
69
69
|
// Remote endpoints
|
|
70
|
+
// Rails expects nested attributes to use the _attributes suffix for has_many associations
|
|
70
71
|
if (params.remote !== undefined && params.remote.length > 0) {
|
|
71
72
|
params.remote.forEach((remote, index) => {
|
|
72
73
|
if (remote.id !== undefined) {
|
|
73
|
-
formData.append(`mcp_implementation[
|
|
74
|
+
formData.append(`mcp_implementation[remote_attributes][${index}][id]`, remote.id.toString());
|
|
74
75
|
}
|
|
75
76
|
if (remote.url_direct !== undefined) {
|
|
76
|
-
formData.append(`mcp_implementation[
|
|
77
|
+
formData.append(`mcp_implementation[remote_attributes][${index}][url_direct]`, remote.url_direct);
|
|
77
78
|
}
|
|
78
79
|
if (remote.url_setup !== undefined) {
|
|
79
|
-
formData.append(`mcp_implementation[
|
|
80
|
+
formData.append(`mcp_implementation[remote_attributes][${index}][url_setup]`, remote.url_setup);
|
|
80
81
|
}
|
|
81
82
|
if (remote.transport !== undefined) {
|
|
82
|
-
formData.append(`mcp_implementation[
|
|
83
|
+
formData.append(`mcp_implementation[remote_attributes][${index}][transport]`, remote.transport);
|
|
83
84
|
}
|
|
84
85
|
if (remote.host_platform !== undefined) {
|
|
85
|
-
formData.append(`mcp_implementation[
|
|
86
|
+
formData.append(`mcp_implementation[remote_attributes][${index}][host_platform]`, remote.host_platform);
|
|
86
87
|
}
|
|
87
88
|
if (remote.host_infrastructure !== undefined) {
|
|
88
|
-
formData.append(`mcp_implementation[
|
|
89
|
+
formData.append(`mcp_implementation[remote_attributes][${index}][host_infrastructure]`, remote.host_infrastructure);
|
|
89
90
|
}
|
|
90
91
|
if (remote.authentication_method !== undefined) {
|
|
91
|
-
formData.append(`mcp_implementation[
|
|
92
|
+
formData.append(`mcp_implementation[remote_attributes][${index}][authentication_method]`, remote.authentication_method);
|
|
92
93
|
}
|
|
93
94
|
if (remote.cost !== undefined) {
|
|
94
|
-
formData.append(`mcp_implementation[
|
|
95
|
+
formData.append(`mcp_implementation[remote_attributes][${index}][cost]`, remote.cost);
|
|
95
96
|
}
|
|
96
97
|
if (remote.status !== undefined) {
|
|
97
|
-
formData.append(`mcp_implementation[
|
|
98
|
+
formData.append(`mcp_implementation[remote_attributes][${index}][status]`, remote.status);
|
|
98
99
|
}
|
|
99
100
|
if (remote.display_name !== undefined) {
|
|
100
|
-
formData.append(`mcp_implementation[
|
|
101
|
+
formData.append(`mcp_implementation[remote_attributes][${index}][display_name]`, remote.display_name);
|
|
101
102
|
}
|
|
102
103
|
if (remote.internal_notes !== undefined) {
|
|
103
|
-
formData.append(`mcp_implementation[
|
|
104
|
+
formData.append(`mcp_implementation[remote_attributes][${index}][internal_notes]`, remote.internal_notes);
|
|
104
105
|
}
|
|
105
106
|
});
|
|
106
107
|
}
|
|
107
108
|
// Canonical URLs
|
|
109
|
+
// Rails expects nested attributes to use the _attributes suffix for has_many associations
|
|
108
110
|
if (params.canonical !== undefined && params.canonical.length > 0) {
|
|
109
111
|
params.canonical.forEach((canonicalUrl, index) => {
|
|
110
|
-
formData.append(`mcp_implementation[
|
|
111
|
-
formData.append(`mcp_implementation[
|
|
112
|
+
formData.append(`mcp_implementation[canonical_attributes][${index}][url]`, canonicalUrl.url);
|
|
113
|
+
formData.append(`mcp_implementation[canonical_attributes][${index}][scope]`, canonicalUrl.scope);
|
|
112
114
|
if (canonicalUrl.note !== undefined) {
|
|
113
|
-
formData.append(`mcp_implementation[
|
|
115
|
+
formData.append(`mcp_implementation[canonical_attributes][${index}][note]`, canonicalUrl.note);
|
|
114
116
|
}
|
|
115
117
|
});
|
|
116
118
|
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { ProvidersResponse } from '../../types.js';
|
|
2
|
+
export declare function searchProviders(apiKey: string, baseUrl: string, params: {
|
|
3
|
+
query: string;
|
|
4
|
+
limit?: number;
|
|
5
|
+
offset?: number;
|
|
6
|
+
}): Promise<ProvidersResponse>;
|
|
7
|
+
//# sourceMappingURL=search-providers.d.ts.map
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
export async function searchProviders(apiKey, baseUrl, params) {
|
|
2
|
+
// Endpoint implemented at: /api/providers/search
|
|
3
|
+
// Requires admin authentication via X-API-Key header
|
|
4
|
+
const url = new URL('/api/providers/search', baseUrl);
|
|
5
|
+
// Add query parameters
|
|
6
|
+
url.searchParams.append('q', params.query);
|
|
7
|
+
if (params.limit) {
|
|
8
|
+
url.searchParams.append('limit', params.limit.toString());
|
|
9
|
+
}
|
|
10
|
+
if (params.offset) {
|
|
11
|
+
url.searchParams.append('offset', params.offset.toString());
|
|
12
|
+
}
|
|
13
|
+
const response = await fetch(url.toString(), {
|
|
14
|
+
method: 'GET',
|
|
15
|
+
headers: {
|
|
16
|
+
'X-API-Key': apiKey,
|
|
17
|
+
Accept: 'application/json',
|
|
18
|
+
},
|
|
19
|
+
});
|
|
20
|
+
if (!response.ok) {
|
|
21
|
+
if (response.status === 401) {
|
|
22
|
+
throw new Error('Invalid API key');
|
|
23
|
+
}
|
|
24
|
+
if (response.status === 403) {
|
|
25
|
+
throw new Error('User lacks admin privileges');
|
|
26
|
+
}
|
|
27
|
+
if (response.status === 404) {
|
|
28
|
+
throw new Error('Providers search endpoint not found');
|
|
29
|
+
}
|
|
30
|
+
throw new Error(`Failed to search providers: ${response.status} ${response.statusText}`);
|
|
31
|
+
}
|
|
32
|
+
const data = (await response.json());
|
|
33
|
+
return {
|
|
34
|
+
providers: data.data || [],
|
|
35
|
+
pagination: data.meta
|
|
36
|
+
? {
|
|
37
|
+
current_page: data.meta.current_page,
|
|
38
|
+
total_pages: data.meta.total_pages,
|
|
39
|
+
total_count: data.meta.total_count,
|
|
40
|
+
has_next: data.meta.has_next,
|
|
41
|
+
limit: data.meta.limit,
|
|
42
|
+
}
|
|
43
|
+
: undefined,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { IPulseMCPAdminClient } from '../server.js';
|
|
2
|
-
import type { Post, ImageUploadResponse, Author, MCPServer, MCPClient, MCPImplementation, MCPImplementationsResponse } from '../types.js';
|
|
2
|
+
import type { Post, ImageUploadResponse, Author, MCPServer, MCPClient, MCPImplementation, MCPImplementationsResponse, Provider, ProvidersResponse } from '../types.js';
|
|
3
3
|
interface MockData {
|
|
4
4
|
posts?: Post[];
|
|
5
5
|
postsBySlug?: Record<string, Post>;
|
|
@@ -13,6 +13,8 @@ interface MockData {
|
|
|
13
13
|
implementations?: MCPImplementation[];
|
|
14
14
|
implementationsResponse?: MCPImplementationsResponse;
|
|
15
15
|
draftImplementationsResponse?: MCPImplementationsResponse;
|
|
16
|
+
providers?: Provider[];
|
|
17
|
+
providersResponse?: ProvidersResponse;
|
|
16
18
|
errors?: {
|
|
17
19
|
getPosts?: Error;
|
|
18
20
|
getPost?: Error;
|
|
@@ -27,6 +29,8 @@ interface MockData {
|
|
|
27
29
|
getDraftMCPImplementations?: Error;
|
|
28
30
|
saveMCPImplementation?: Error;
|
|
29
31
|
sendEmail?: Error;
|
|
32
|
+
searchProviders?: Error;
|
|
33
|
+
getProviderById?: Error;
|
|
30
34
|
};
|
|
31
35
|
}
|
|
32
36
|
export declare function createMockPulseMCPAdminClient(mockData: MockData): IPulseMCPAdminClient;
|
|
@@ -334,5 +334,68 @@ export function createMockPulseMCPAdminClient(mockData) {
|
|
|
334
334
|
updated_at: new Date().toISOString(),
|
|
335
335
|
};
|
|
336
336
|
},
|
|
337
|
+
async searchProviders(params) {
|
|
338
|
+
if (mockData.errors?.searchProviders) {
|
|
339
|
+
throw mockData.errors.searchProviders;
|
|
340
|
+
}
|
|
341
|
+
if (mockData.providersResponse) {
|
|
342
|
+
return mockData.providersResponse;
|
|
343
|
+
}
|
|
344
|
+
let providers = mockData.providers || [
|
|
345
|
+
{
|
|
346
|
+
id: 1,
|
|
347
|
+
name: 'Test Provider',
|
|
348
|
+
slug: 'test-provider',
|
|
349
|
+
url: 'https://testprovider.com',
|
|
350
|
+
implementations_count: 5,
|
|
351
|
+
created_at: '2024-01-01T00:00:00Z',
|
|
352
|
+
updated_at: '2024-01-01T00:00:00Z',
|
|
353
|
+
},
|
|
354
|
+
];
|
|
355
|
+
// Apply search filter
|
|
356
|
+
if (params.query) {
|
|
357
|
+
const query = params.query.toLowerCase();
|
|
358
|
+
providers = providers.filter((provider) => provider.name.toLowerCase().includes(query) ||
|
|
359
|
+
provider.slug.toLowerCase().includes(query) ||
|
|
360
|
+
(provider.url && provider.url.toLowerCase().includes(query)));
|
|
361
|
+
}
|
|
362
|
+
// Apply pagination
|
|
363
|
+
const offset = params.offset || 0;
|
|
364
|
+
const limit = params.limit || 30;
|
|
365
|
+
const totalCount = providers.length;
|
|
366
|
+
const paginatedProviders = providers.slice(offset, offset + limit);
|
|
367
|
+
return {
|
|
368
|
+
providers: paginatedProviders,
|
|
369
|
+
pagination: {
|
|
370
|
+
current_page: Math.floor(offset / limit) + 1,
|
|
371
|
+
total_pages: Math.ceil(totalCount / limit),
|
|
372
|
+
total_count: totalCount,
|
|
373
|
+
has_next: offset + limit < totalCount,
|
|
374
|
+
limit: limit,
|
|
375
|
+
},
|
|
376
|
+
};
|
|
377
|
+
},
|
|
378
|
+
async getProviderById(id) {
|
|
379
|
+
if (mockData.errors?.getProviderById) {
|
|
380
|
+
throw mockData.errors.getProviderById;
|
|
381
|
+
}
|
|
382
|
+
// Find provider in mock data by ID
|
|
383
|
+
const providers = mockData.providers || [
|
|
384
|
+
{
|
|
385
|
+
id: 1,
|
|
386
|
+
name: 'Test Provider',
|
|
387
|
+
slug: 'test-provider',
|
|
388
|
+
url: 'https://testprovider.com',
|
|
389
|
+
implementations_count: 5,
|
|
390
|
+
created_at: '2024-01-01T00:00:00Z',
|
|
391
|
+
updated_at: '2024-01-01T00:00:00Z',
|
|
392
|
+
},
|
|
393
|
+
];
|
|
394
|
+
const found = providers.find((p) => p.id === id);
|
|
395
|
+
if (found)
|
|
396
|
+
return found;
|
|
397
|
+
// Return null if not found
|
|
398
|
+
return null;
|
|
399
|
+
},
|
|
337
400
|
};
|
|
338
401
|
}
|
package/shared/server.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
2
|
-
import type { Post, PostsResponse, CreatePostParams, UpdatePostParams, ImageUploadResponse, Author, AuthorsResponse, MCPServer, MCPClient, MCPImplementation, MCPImplementationsResponse, SaveMCPImplementationParams } from './types.js';
|
|
2
|
+
import type { Post, PostsResponse, CreatePostParams, UpdatePostParams, ImageUploadResponse, Author, AuthorsResponse, MCPServer, MCPClient, MCPImplementation, MCPImplementationsResponse, SaveMCPImplementationParams, Provider, ProvidersResponse } from './types.js';
|
|
3
3
|
export interface IPulseMCPAdminClient {
|
|
4
4
|
getPosts(params?: {
|
|
5
5
|
search?: string;
|
|
@@ -54,6 +54,12 @@ export interface IPulseMCPAdminClient {
|
|
|
54
54
|
created_at: string;
|
|
55
55
|
updated_at: string;
|
|
56
56
|
}>;
|
|
57
|
+
searchProviders(params: {
|
|
58
|
+
query: string;
|
|
59
|
+
limit?: number;
|
|
60
|
+
offset?: number;
|
|
61
|
+
}): Promise<ProvidersResponse>;
|
|
62
|
+
getProviderById(id: number): Promise<Provider | null>;
|
|
57
63
|
}
|
|
58
64
|
export declare class PulseMCPAdminClient implements IPulseMCPAdminClient {
|
|
59
65
|
private apiKey;
|
|
@@ -112,6 +118,12 @@ export declare class PulseMCPAdminClient implements IPulseMCPAdminClient {
|
|
|
112
118
|
created_at: string;
|
|
113
119
|
updated_at: string;
|
|
114
120
|
}>;
|
|
121
|
+
searchProviders(params: {
|
|
122
|
+
query: string;
|
|
123
|
+
limit?: number;
|
|
124
|
+
offset?: number;
|
|
125
|
+
}): Promise<ProvidersResponse>;
|
|
126
|
+
getProviderById(id: number): Promise<Provider | null>;
|
|
115
127
|
}
|
|
116
128
|
export type ClientFactory = () => IPulseMCPAdminClient;
|
|
117
129
|
export declare function createMCPServer(): {
|
package/shared/server.js
CHANGED
|
@@ -75,6 +75,14 @@ export class PulseMCPAdminClient {
|
|
|
75
75
|
const { sendEmail } = await import('./pulsemcp-admin-client/lib/send-email.js');
|
|
76
76
|
return sendEmail(this.apiKey, this.baseUrl, params);
|
|
77
77
|
}
|
|
78
|
+
async searchProviders(params) {
|
|
79
|
+
const { searchProviders } = await import('./pulsemcp-admin-client/lib/search-providers.js');
|
|
80
|
+
return searchProviders(this.apiKey, this.baseUrl, params);
|
|
81
|
+
}
|
|
82
|
+
async getProviderById(id) {
|
|
83
|
+
const { getProviderById } = await import('./pulsemcp-admin-client/lib/get-provider-by-id.js');
|
|
84
|
+
return getProviderById(this.apiKey, this.baseUrl, id);
|
|
85
|
+
}
|
|
78
86
|
}
|
|
79
87
|
export function createMCPServer() {
|
|
80
88
|
const server = new Server({
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
2
|
+
import type { ClientFactory } from '../server.js';
|
|
3
|
+
export declare function findProviders(_server: Server, clientFactory: ClientFactory): {
|
|
4
|
+
name: string;
|
|
5
|
+
description: string;
|
|
6
|
+
inputSchema: {
|
|
7
|
+
type: string;
|
|
8
|
+
properties: {
|
|
9
|
+
id: {
|
|
10
|
+
type: string;
|
|
11
|
+
description: "Provider ID to retrieve a specific provider";
|
|
12
|
+
};
|
|
13
|
+
query: {
|
|
14
|
+
type: string;
|
|
15
|
+
description: "Search query to find providers by name, URL, or slug";
|
|
16
|
+
};
|
|
17
|
+
limit: {
|
|
18
|
+
type: string;
|
|
19
|
+
description: "Maximum number of results to return (1-100, default: 30)";
|
|
20
|
+
};
|
|
21
|
+
offset: {
|
|
22
|
+
type: string;
|
|
23
|
+
description: "Number of results to skip for pagination (default: 0)";
|
|
24
|
+
};
|
|
25
|
+
};
|
|
26
|
+
};
|
|
27
|
+
handler: (args: unknown) => Promise<{
|
|
28
|
+
content: {
|
|
29
|
+
type: string;
|
|
30
|
+
text: string;
|
|
31
|
+
}[];
|
|
32
|
+
isError?: undefined;
|
|
33
|
+
} | {
|
|
34
|
+
content: {
|
|
35
|
+
type: string;
|
|
36
|
+
text: string;
|
|
37
|
+
}[];
|
|
38
|
+
isError: boolean;
|
|
39
|
+
}>;
|
|
40
|
+
};
|
|
41
|
+
//# sourceMappingURL=find-providers.d.ts.map
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
// Parameter descriptions - single source of truth
|
|
3
|
+
const PARAM_DESCRIPTIONS = {
|
|
4
|
+
id: 'Provider ID to retrieve a specific provider',
|
|
5
|
+
query: 'Search query to find providers by name, URL, or slug',
|
|
6
|
+
limit: 'Maximum number of results to return (1-100, default: 30)',
|
|
7
|
+
offset: 'Number of results to skip for pagination (default: 0)',
|
|
8
|
+
};
|
|
9
|
+
const FindProvidersSchema = z
|
|
10
|
+
.object({
|
|
11
|
+
id: z.number().int().positive().optional().describe(PARAM_DESCRIPTIONS.id),
|
|
12
|
+
query: z.string().min(1).optional().describe(PARAM_DESCRIPTIONS.query),
|
|
13
|
+
limit: z.number().min(1).max(100).optional().describe(PARAM_DESCRIPTIONS.limit),
|
|
14
|
+
offset: z.number().min(0).optional().describe(PARAM_DESCRIPTIONS.offset),
|
|
15
|
+
})
|
|
16
|
+
.refine((data) => data.id !== undefined || data.query !== undefined, {
|
|
17
|
+
message: 'Either id or query must be provided',
|
|
18
|
+
});
|
|
19
|
+
export function findProviders(_server, clientFactory) {
|
|
20
|
+
return {
|
|
21
|
+
name: 'find_providers',
|
|
22
|
+
description: `Find providers (organizations/individuals) in the PulseMCP registry.
|
|
23
|
+
|
|
24
|
+
This tool can operate in two modes:
|
|
25
|
+
|
|
26
|
+
1. **Find by ID**: Retrieve a specific provider by its numeric ID
|
|
27
|
+
- Returns a single provider with detailed information
|
|
28
|
+
- Returns null if not found
|
|
29
|
+
|
|
30
|
+
2. **Search by query**: Search for providers by name, URL, or slug
|
|
31
|
+
- Searches across provider name, URL, and slug fields (case-insensitive)
|
|
32
|
+
- Returns a list of matching providers with pagination support
|
|
33
|
+
- Each result includes implementation counts
|
|
34
|
+
|
|
35
|
+
Provider information includes:
|
|
36
|
+
- ID and slug
|
|
37
|
+
- Name and URL
|
|
38
|
+
- Number of associated implementations
|
|
39
|
+
- Creation and update timestamps
|
|
40
|
+
|
|
41
|
+
Use cases:
|
|
42
|
+
- Look up a specific provider by ID
|
|
43
|
+
- Search for providers by name (e.g., "anthropic", "modelcontextprotocol")
|
|
44
|
+
- Find providers by partial name matches
|
|
45
|
+
- Discover all providers for an organization
|
|
46
|
+
- Check how many implementations a provider has published
|
|
47
|
+
|
|
48
|
+
Note: This tool queries the PulseMCP registry API. Results depend on what has been published to the registry.`,
|
|
49
|
+
inputSchema: {
|
|
50
|
+
type: 'object',
|
|
51
|
+
properties: {
|
|
52
|
+
id: {
|
|
53
|
+
type: 'number',
|
|
54
|
+
description: PARAM_DESCRIPTIONS.id,
|
|
55
|
+
},
|
|
56
|
+
query: {
|
|
57
|
+
type: 'string',
|
|
58
|
+
description: PARAM_DESCRIPTIONS.query,
|
|
59
|
+
},
|
|
60
|
+
limit: {
|
|
61
|
+
type: 'number',
|
|
62
|
+
description: PARAM_DESCRIPTIONS.limit,
|
|
63
|
+
},
|
|
64
|
+
offset: {
|
|
65
|
+
type: 'number',
|
|
66
|
+
description: PARAM_DESCRIPTIONS.offset,
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
handler: async (args) => {
|
|
71
|
+
const validatedArgs = FindProvidersSchema.parse(args);
|
|
72
|
+
const client = clientFactory();
|
|
73
|
+
try {
|
|
74
|
+
// Mode 1: Find by ID
|
|
75
|
+
if (validatedArgs.id !== undefined) {
|
|
76
|
+
const provider = await client.getProviderById(validatedArgs.id);
|
|
77
|
+
if (!provider) {
|
|
78
|
+
return {
|
|
79
|
+
content: [
|
|
80
|
+
{
|
|
81
|
+
type: 'text',
|
|
82
|
+
text: `Provider with ID ${validatedArgs.id} not found.`,
|
|
83
|
+
},
|
|
84
|
+
],
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
let content = `**${provider.name}**\n`;
|
|
88
|
+
content += `ID: ${provider.id}\n`;
|
|
89
|
+
content += `Slug: ${provider.slug}\n`;
|
|
90
|
+
if (provider.url) {
|
|
91
|
+
content += `URL: ${provider.url}\n`;
|
|
92
|
+
}
|
|
93
|
+
if (provider.implementations_count !== undefined) {
|
|
94
|
+
content += `Implementations: ${provider.implementations_count}\n`;
|
|
95
|
+
}
|
|
96
|
+
if (provider.created_at) {
|
|
97
|
+
content += `Created: ${provider.created_at}\n`;
|
|
98
|
+
}
|
|
99
|
+
return {
|
|
100
|
+
content: [
|
|
101
|
+
{
|
|
102
|
+
type: 'text',
|
|
103
|
+
text: content.trim(),
|
|
104
|
+
},
|
|
105
|
+
],
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
// Mode 2: Search by query
|
|
109
|
+
if (validatedArgs.query !== undefined) {
|
|
110
|
+
const response = await client.searchProviders({
|
|
111
|
+
query: validatedArgs.query,
|
|
112
|
+
limit: validatedArgs.limit,
|
|
113
|
+
offset: validatedArgs.offset,
|
|
114
|
+
});
|
|
115
|
+
let content = `Found ${response.providers.length} provider(s) matching "${validatedArgs.query}"`;
|
|
116
|
+
if (response.pagination) {
|
|
117
|
+
content += ` (showing ${response.providers.length} of ${response.pagination.total_count} total)`;
|
|
118
|
+
}
|
|
119
|
+
content += ':\n\n';
|
|
120
|
+
for (const [index, provider] of response.providers.entries()) {
|
|
121
|
+
content += `${index + 1}. **${provider.name}**\n`;
|
|
122
|
+
content += ` ID: ${provider.id} | Slug: ${provider.slug}\n`;
|
|
123
|
+
if (provider.url) {
|
|
124
|
+
content += ` URL: ${provider.url}\n`;
|
|
125
|
+
}
|
|
126
|
+
if (provider.implementations_count !== undefined) {
|
|
127
|
+
content += ` Implementations: ${provider.implementations_count}\n`;
|
|
128
|
+
}
|
|
129
|
+
content += '\n';
|
|
130
|
+
}
|
|
131
|
+
if (response.pagination?.has_next) {
|
|
132
|
+
const nextOffset = (validatedArgs.offset || 0) + (validatedArgs.limit || 30);
|
|
133
|
+
content += `\n---\nMore results available. Use offset=${nextOffset} to see the next page.`;
|
|
134
|
+
}
|
|
135
|
+
return {
|
|
136
|
+
content: [
|
|
137
|
+
{
|
|
138
|
+
type: 'text',
|
|
139
|
+
text: content.trim(),
|
|
140
|
+
},
|
|
141
|
+
],
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
// This should never happen due to zod refinement, but TypeScript needs it
|
|
145
|
+
throw new Error('Either id or query must be provided');
|
|
146
|
+
}
|
|
147
|
+
catch (error) {
|
|
148
|
+
return {
|
|
149
|
+
content: [
|
|
150
|
+
{
|
|
151
|
+
type: 'text',
|
|
152
|
+
text: `Error finding providers: ${error instanceof Error ? error.message : String(error)}`,
|
|
153
|
+
},
|
|
154
|
+
],
|
|
155
|
+
isError: true,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
},
|
|
159
|
+
};
|
|
160
|
+
}
|
|
@@ -38,6 +38,8 @@ Returns a list of matching implementations with their metadata, including:
|
|
|
38
38
|
- GitHub stars and popularity metrics
|
|
39
39
|
- Associated MCP server/client IDs
|
|
40
40
|
- PulseMCP web URL
|
|
41
|
+
- Remote endpoints (for servers) - hosting platforms, transport methods, authentication
|
|
42
|
+
- Canonical URLs - authoritative sources for the implementation
|
|
41
43
|
|
|
42
44
|
Use cases:
|
|
43
45
|
- Find existing MCP servers before creating a new one
|
|
@@ -96,6 +98,7 @@ Note: This tool queries the PulseMCP registry API. Results depend on what has be
|
|
|
96
98
|
content += ':\n\n';
|
|
97
99
|
for (const [index, impl] of response.implementations.entries()) {
|
|
98
100
|
content += `${index + 1}. **${impl.name}** (${impl.type})\n`;
|
|
101
|
+
content += ` ID: ${impl.id}\n`;
|
|
99
102
|
content += ` Slug: ${impl.slug}\n`;
|
|
100
103
|
content += ` Status: ${impl.status}`;
|
|
101
104
|
if (impl.classification) {
|
|
@@ -124,6 +127,40 @@ Note: This tool queries the PulseMCP registry API. Results depend on what has be
|
|
|
124
127
|
if (impl.mcp_client_id) {
|
|
125
128
|
content += ` MCP Client ID: ${impl.mcp_client_id}\n`;
|
|
126
129
|
}
|
|
130
|
+
// Display remote endpoints if available
|
|
131
|
+
if (impl.mcp_server?.remotes && impl.mcp_server.remotes.length > 0) {
|
|
132
|
+
content += ` Remotes (${impl.mcp_server.remotes.length}):\n`;
|
|
133
|
+
for (const remote of impl.mcp_server.remotes) {
|
|
134
|
+
let remoteLine = ` - ${remote.display_name || remote.url_direct || remote.url_setup || 'Unnamed'}`;
|
|
135
|
+
const attrs = [];
|
|
136
|
+
if (remote.transport)
|
|
137
|
+
attrs.push(remote.transport);
|
|
138
|
+
if (remote.host_platform)
|
|
139
|
+
attrs.push(remote.host_platform);
|
|
140
|
+
if (remote.authentication_method)
|
|
141
|
+
attrs.push(`auth: ${remote.authentication_method}`);
|
|
142
|
+
if (remote.cost)
|
|
143
|
+
attrs.push(remote.cost);
|
|
144
|
+
if (attrs.length > 0) {
|
|
145
|
+
remoteLine += ` [${attrs.join(', ')}]`;
|
|
146
|
+
}
|
|
147
|
+
content += remoteLine + '\n';
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
else if (impl.mcp_server?.mcp_server_remotes_count) {
|
|
151
|
+
content += ` Remotes Count: ${impl.mcp_server.mcp_server_remotes_count}\n`;
|
|
152
|
+
}
|
|
153
|
+
// Display canonical URLs if available
|
|
154
|
+
if (impl.canonical && impl.canonical.length > 0) {
|
|
155
|
+
content += ` Canonical URLs (${impl.canonical.length}):\n`;
|
|
156
|
+
for (const canonical of impl.canonical) {
|
|
157
|
+
let canonicalLine = ` - ${canonical.url} [scope: ${canonical.scope}]`;
|
|
158
|
+
if (canonical.note) {
|
|
159
|
+
canonicalLine += ` - ${canonical.note}`;
|
|
160
|
+
}
|
|
161
|
+
content += canonicalLine + '\n';
|
|
162
|
+
}
|
|
163
|
+
}
|
|
127
164
|
content += '\n';
|
|
128
165
|
}
|
|
129
166
|
if (response.pagination?.has_next) {
|
package/shared/tools.js
CHANGED
|
@@ -9,6 +9,7 @@ import { searchMCPImplementations } from './tools/search-mcp-implementations.js'
|
|
|
9
9
|
import { getDraftMCPImplementations } from './tools/get-draft-mcp-implementations.js';
|
|
10
10
|
import { saveMCPImplementation } from './tools/save-mcp-implementation.js';
|
|
11
11
|
import { sendMCPImplementationPostingNotification } from './tools/send-mcp-implementation-posting-notification.js';
|
|
12
|
+
import { findProviders } from './tools/find-providers.js';
|
|
12
13
|
const ALL_TOOLS = [
|
|
13
14
|
{ factory: getNewsletterPosts, groups: ['newsletter'] },
|
|
14
15
|
{ factory: getNewsletterPost, groups: ['newsletter'] },
|
|
@@ -20,6 +21,7 @@ const ALL_TOOLS = [
|
|
|
20
21
|
{ factory: getDraftMCPImplementations, groups: ['server_queue_readonly', 'server_queue_all'] },
|
|
21
22
|
{ factory: saveMCPImplementation, groups: ['server_queue_all'] },
|
|
22
23
|
{ factory: sendMCPImplementationPostingNotification, groups: ['server_queue_all'] },
|
|
24
|
+
{ factory: findProviders, groups: ['server_queue_readonly', 'server_queue_all'] },
|
|
23
25
|
];
|
|
24
26
|
/**
|
|
25
27
|
* Parse enabled tool groups from environment variable or parameter
|
package/shared/types.d.ts
CHANGED
|
@@ -164,6 +164,7 @@ export interface MCPImplementation {
|
|
|
164
164
|
updated_at?: string;
|
|
165
165
|
mcp_server?: MCPServer | null;
|
|
166
166
|
mcp_client?: MCPClient | null;
|
|
167
|
+
canonical?: CanonicalUrlParams[];
|
|
167
168
|
}
|
|
168
169
|
export interface MCPImplementationsResponse {
|
|
169
170
|
implementations: MCPImplementation[];
|
|
@@ -199,4 +200,23 @@ export interface SaveMCPImplementationParams {
|
|
|
199
200
|
canonical?: CanonicalUrlParams[];
|
|
200
201
|
internal_notes?: string;
|
|
201
202
|
}
|
|
203
|
+
export interface Provider {
|
|
204
|
+
id: number;
|
|
205
|
+
name: string;
|
|
206
|
+
slug: string;
|
|
207
|
+
url?: string;
|
|
208
|
+
implementations_count?: number;
|
|
209
|
+
created_at?: string;
|
|
210
|
+
updated_at?: string;
|
|
211
|
+
}
|
|
212
|
+
export interface ProvidersResponse {
|
|
213
|
+
providers: Provider[];
|
|
214
|
+
pagination?: {
|
|
215
|
+
current_page: number;
|
|
216
|
+
total_pages: number;
|
|
217
|
+
total_count: number;
|
|
218
|
+
has_next?: boolean;
|
|
219
|
+
limit?: number;
|
|
220
|
+
};
|
|
221
|
+
}
|
|
202
222
|
//# sourceMappingURL=types.d.ts.map
|