@coldiq/mcp 5.4.4 → 5.4.6

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.
Files changed (71) hide show
  1. package/dist/index.js +44 -36
  2. package/dist/index.js.map +1 -1
  3. package/dist/tool-json-schema.d.ts +8 -0
  4. package/dist/tool-json-schema.d.ts.map +1 -0
  5. package/dist/tool-json-schema.js +19 -0
  6. package/dist/tool-json-schema.js.map +1 -0
  7. package/dist/tools/call-endpoint.d.ts +7 -1
  8. package/dist/tools/call-endpoint.d.ts.map +1 -1
  9. package/dist/tools/call-endpoint.js +1 -1
  10. package/dist/tools/call-endpoint.js.map +1 -1
  11. package/dist/tools/enrich-company-bulk.d.ts +1 -9
  12. package/dist/tools/enrich-company-bulk.d.ts.map +1 -1
  13. package/dist/tools/enrich-company.d.ts +1 -1
  14. package/dist/tools/enrich-person-bulk.d.ts +1 -15
  15. package/dist/tools/enrich-person-bulk.d.ts.map +1 -1
  16. package/dist/tools/enrich-person.d.ts +1 -1
  17. package/dist/tools/extract-post-engagement.d.ts +5 -1
  18. package/dist/tools/extract-post-engagement.d.ts.map +1 -1
  19. package/dist/tools/fetch-page-content.d.ts +2 -2
  20. package/dist/tools/find-email.d.ts +1 -1
  21. package/dist/tools/find-emails-bulk.d.ts +1 -15
  22. package/dist/tools/find-emails-bulk.d.ts.map +1 -1
  23. package/dist/tools/find-emails.d.ts +2 -14
  24. package/dist/tools/find-emails.d.ts.map +1 -1
  25. package/dist/tools/find-influencers.d.ts +28 -8
  26. package/dist/tools/find-influencers.d.ts.map +1 -1
  27. package/dist/tools/find-influencers.js +1 -1
  28. package/dist/tools/find-influencers.js.map +1 -1
  29. package/dist/tools/find-people.d.ts +11 -8
  30. package/dist/tools/find-people.d.ts.map +1 -1
  31. package/dist/tools/find-phone-bulk.d.ts +1 -13
  32. package/dist/tools/find-phone-bulk.d.ts.map +1 -1
  33. package/dist/tools/find-phone.d.ts +1 -1
  34. package/dist/tools/find-signals.d.ts +16 -8
  35. package/dist/tools/find-signals.d.ts.map +1 -1
  36. package/dist/tools/get-endpoint-details.d.ts +7 -1
  37. package/dist/tools/get-endpoint-details.d.ts.map +1 -1
  38. package/dist/tools/get-place-reviews.d.ts +8 -3
  39. package/dist/tools/get-place-reviews.d.ts.map +1 -1
  40. package/dist/tools/get-website-visitors.d.ts +4 -1
  41. package/dist/tools/get-website-visitors.d.ts.map +1 -1
  42. package/dist/tools/search-ads.d.ts +14 -6
  43. package/dist/tools/search-ads.d.ts.map +1 -1
  44. package/dist/tools/search-companies.d.ts +15 -12
  45. package/dist/tools/search-companies.d.ts.map +1 -1
  46. package/dist/tools/search-jobs.d.ts +54 -22
  47. package/dist/tools/search-jobs.d.ts.map +1 -1
  48. package/dist/tools/search-knowledge.d.ts +6 -2
  49. package/dist/tools/search-knowledge.d.ts.map +1 -1
  50. package/dist/tools/search-knowledge.js +1 -1
  51. package/dist/tools/search-knowledge.js.map +1 -1
  52. package/dist/tools/search-places.d.ts +16 -8
  53. package/dist/tools/search-places.d.ts.map +1 -1
  54. package/dist/tools/search-reddit.d.ts +24 -5
  55. package/dist/tools/search-reddit.d.ts.map +1 -1
  56. package/dist/tools/search-seo.d.ts +33 -8
  57. package/dist/tools/search-seo.d.ts.map +1 -1
  58. package/dist/tools/search-web.d.ts +7 -2
  59. package/dist/tools/search-web.d.ts.map +1 -1
  60. package/dist/tools/verify-email.d.ts +1 -1
  61. package/dist/tools/verify-emails-bulk.d.ts +2 -2
  62. package/package.json +7 -5
  63. package/src/index.ts +51 -37
  64. package/src/tool-json-schema.ts +21 -0
  65. package/src/tools/call-endpoint.ts +1 -1
  66. package/src/tools/find-influencers.ts +1 -1
  67. package/src/tools/search-knowledge.ts +1 -1
  68. package/tests/gtm/harness.ts +2 -2
  69. package/tests/instructions.test.ts +3 -3
  70. package/tests/sdk-zod4-compatibility.test.ts +59 -0
  71. package/tests/tool-json-schema.test.ts +94 -0
package/src/index.ts CHANGED
@@ -1,7 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
3
+ import { McpServer, type ToolCallback } from '@modelcontextprotocol/sdk/server/mcp.js'
4
4
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
5
+ import { z } from 'zod'
5
6
  import { initClient } from './client.js'
6
7
  import { MCP_SERVER_INSTRUCTIONS } from './instructions.js'
7
8
  import { withKnowledgeFirstRequirement } from './knowledge-policy.js'
@@ -220,48 +221,61 @@ const server = new McpServer(
220
221
  },
221
222
  )
222
223
 
224
+ function registerPublicTool<Fields extends Record<string, z.ZodType>>(
225
+ name: string,
226
+ description: string,
227
+ schema: Fields,
228
+ handler: ToolCallback<z.ZodObject<Fields>>,
229
+ ): void {
230
+ const inputSchema = z.strictObject(schema)
231
+ server.registerTool<never, typeof inputSchema>(name, {
232
+ description,
233
+ inputSchema,
234
+ }, handler)
235
+ }
236
+
223
237
  // Register knowledge retrieval before every operational tool so clients that
224
238
  // preserve registration order surface the mandatory prerequisite first.
225
- server.tool(searchKnowledgeName, searchKnowledgeDescription, searchKnowledgeSchema, searchKnowledgeHandler)
226
- server.tool(searchCompaniesName, withKnowledgeFirstRequirement(searchCompaniesDescription), searchCompaniesSchema, searchCompaniesHandler)
227
- server.tool(findPeopleName, withKnowledgeFirstRequirement(findPeopleDescription), findPeopleSchema, findPeopleHandler)
228
- server.tool(findEmailName, withKnowledgeFirstRequirement(findEmailDescription), findEmailSchema, findEmailHandler)
229
- server.tool(enrichPersonName, withKnowledgeFirstRequirement(enrichPersonDescription), enrichPersonSchema, enrichPersonHandler)
230
- server.tool(findEmailsName, withKnowledgeFirstRequirement(findEmailsDescription), findEmailsSchema, findEmailsHandler)
231
- server.tool(verifyEmailName, withKnowledgeFirstRequirement(verifyEmailDescription), verifyEmailSchema, verifyEmailHandler)
232
- server.tool(verifyEmailsBulkName, withKnowledgeFirstRequirement(verifyEmailsBulkDescription), verifyEmailsBulkSchema, verifyEmailsBulkHandler)
233
- server.tool(findEmailsBulkName, withKnowledgeFirstRequirement(findEmailsBulkDescription), findEmailsBulkSchema, findEmailsBulkHandler)
234
- server.tool(enrichPersonBulkName, withKnowledgeFirstRequirement(enrichPersonBulkDescription), enrichPersonBulkSchema, enrichPersonBulkHandler)
235
- server.tool(enrichCompanyBulkName, withKnowledgeFirstRequirement(enrichCompanyBulkDescription), enrichCompanyBulkSchema, enrichCompanyBulkHandler)
236
- server.tool(findPhoneBulkName, withKnowledgeFirstRequirement(findPhoneBulkDescription), findPhoneBulkSchema, findPhoneBulkHandler)
237
- server.tool(getBulkJobName, withKnowledgeFirstRequirement(getBulkJobDescription), getBulkJobSchema, getBulkJobHandler)
238
- server.tool(cancelBulkJobName, withKnowledgeFirstRequirement(cancelBulkJobDescription), cancelBulkJobSchema, cancelBulkJobHandler)
239
- server.tool(findPhoneName, withKnowledgeFirstRequirement(findPhoneDescription), findPhoneSchema, findPhoneHandler)
240
- server.tool(enrichCompanyName, withKnowledgeFirstRequirement(enrichCompanyDescription), enrichCompanySchema, enrichCompanyHandler)
241
- server.tool(searchWebName, withKnowledgeFirstRequirement(searchWebDescription), searchWebSchema, searchWebHandler)
242
- server.tool(searchJobsName, withKnowledgeFirstRequirement(searchJobsDescription), searchJobsSchema, searchJobsHandler)
243
- server.tool(searchAdsName, withKnowledgeFirstRequirement(searchAdsDescription), searchAdsSchema, searchAdsHandler)
244
- server.tool(searchPlacesName, withKnowledgeFirstRequirement(searchPlacesDescription), searchPlacesSchema, searchPlacesHandler)
245
- server.tool(findInfluencersName, withKnowledgeFirstRequirement(findInfluencersDescription), findInfluencersSchema, findInfluencersHandler)
246
- server.tool(searchRedditName, withKnowledgeFirstRequirement(searchRedditDescription), searchRedditSchema, searchRedditHandler)
247
- server.tool(searchSeoName, withKnowledgeFirstRequirement(searchSeoDescription), searchSeoSchema, searchSeoHandler)
248
- server.tool(findSignalsName, withKnowledgeFirstRequirement(findSignalsDescription), findSignalsSchema, findSignalsHandler)
249
- server.tool(fetchPageContentName, withKnowledgeFirstRequirement(fetchPageContentDescription), fetchPageContentSchema, fetchPageContentHandler)
250
- server.tool(getCreditBalanceName, getCreditBalanceDescription, getCreditBalanceSchema, getCreditBalanceHandler)
251
- server.tool(getSessionUsageName, getSessionUsageDescription, getSessionUsageSchema, getSessionUsageHandler)
252
- server.tool(listDataSourcesName, withKnowledgeFirstRequirement(listDataSourcesDescription), listDataSourcesSchema, listDataSourcesHandler)
253
- server.tool(listSkillsName, listSkillsDescription, listSkillsSchema, listSkillsHandler)
254
- server.tool(loadSkillName, loadSkillDescription, loadSkillSchema, loadSkillHandler)
255
- server.tool(extractPostEngagementName, withKnowledgeFirstRequirement(extractPostEngagementDescription), extractPostEngagementSchema, extractPostEngagementHandler)
256
- server.tool(getPlaceReviewsName, withKnowledgeFirstRequirement(getPlaceReviewsDescription), getPlaceReviewsSchema, getPlaceReviewsHandler)
239
+ registerPublicTool(searchKnowledgeName, searchKnowledgeDescription, searchKnowledgeSchema, searchKnowledgeHandler)
240
+ registerPublicTool(searchCompaniesName, withKnowledgeFirstRequirement(searchCompaniesDescription), searchCompaniesSchema, searchCompaniesHandler)
241
+ registerPublicTool(findPeopleName, withKnowledgeFirstRequirement(findPeopleDescription), findPeopleSchema, findPeopleHandler)
242
+ registerPublicTool(findEmailName, withKnowledgeFirstRequirement(findEmailDescription), findEmailSchema, findEmailHandler)
243
+ registerPublicTool(enrichPersonName, withKnowledgeFirstRequirement(enrichPersonDescription), enrichPersonSchema, enrichPersonHandler)
244
+ registerPublicTool(findEmailsName, withKnowledgeFirstRequirement(findEmailsDescription), findEmailsSchema, findEmailsHandler)
245
+ registerPublicTool(verifyEmailName, withKnowledgeFirstRequirement(verifyEmailDescription), verifyEmailSchema, verifyEmailHandler)
246
+ registerPublicTool(verifyEmailsBulkName, withKnowledgeFirstRequirement(verifyEmailsBulkDescription), verifyEmailsBulkSchema, verifyEmailsBulkHandler)
247
+ registerPublicTool(findEmailsBulkName, withKnowledgeFirstRequirement(findEmailsBulkDescription), findEmailsBulkSchema, findEmailsBulkHandler)
248
+ registerPublicTool(enrichPersonBulkName, withKnowledgeFirstRequirement(enrichPersonBulkDescription), enrichPersonBulkSchema, enrichPersonBulkHandler)
249
+ registerPublicTool(enrichCompanyBulkName, withKnowledgeFirstRequirement(enrichCompanyBulkDescription), enrichCompanyBulkSchema, enrichCompanyBulkHandler)
250
+ registerPublicTool(findPhoneBulkName, withKnowledgeFirstRequirement(findPhoneBulkDescription), findPhoneBulkSchema, findPhoneBulkHandler)
251
+ registerPublicTool(getBulkJobName, withKnowledgeFirstRequirement(getBulkJobDescription), getBulkJobSchema, getBulkJobHandler)
252
+ registerPublicTool(cancelBulkJobName, withKnowledgeFirstRequirement(cancelBulkJobDescription), cancelBulkJobSchema, cancelBulkJobHandler)
253
+ registerPublicTool(findPhoneName, withKnowledgeFirstRequirement(findPhoneDescription), findPhoneSchema, findPhoneHandler)
254
+ registerPublicTool(enrichCompanyName, withKnowledgeFirstRequirement(enrichCompanyDescription), enrichCompanySchema, enrichCompanyHandler)
255
+ registerPublicTool(searchWebName, withKnowledgeFirstRequirement(searchWebDescription), searchWebSchema, searchWebHandler)
256
+ registerPublicTool(searchJobsName, withKnowledgeFirstRequirement(searchJobsDescription), searchJobsSchema, searchJobsHandler)
257
+ registerPublicTool(searchAdsName, withKnowledgeFirstRequirement(searchAdsDescription), searchAdsSchema, searchAdsHandler)
258
+ registerPublicTool(searchPlacesName, withKnowledgeFirstRequirement(searchPlacesDescription), searchPlacesSchema, searchPlacesHandler)
259
+ registerPublicTool(findInfluencersName, withKnowledgeFirstRequirement(findInfluencersDescription), findInfluencersSchema, findInfluencersHandler)
260
+ registerPublicTool(searchRedditName, withKnowledgeFirstRequirement(searchRedditDescription), searchRedditSchema, searchRedditHandler)
261
+ registerPublicTool(searchSeoName, withKnowledgeFirstRequirement(searchSeoDescription), searchSeoSchema, searchSeoHandler)
262
+ registerPublicTool(findSignalsName, withKnowledgeFirstRequirement(findSignalsDescription), findSignalsSchema, findSignalsHandler)
263
+ registerPublicTool(fetchPageContentName, withKnowledgeFirstRequirement(fetchPageContentDescription), fetchPageContentSchema, fetchPageContentHandler)
264
+ registerPublicTool(getCreditBalanceName, getCreditBalanceDescription, getCreditBalanceSchema, getCreditBalanceHandler)
265
+ registerPublicTool(getSessionUsageName, getSessionUsageDescription, getSessionUsageSchema, getSessionUsageHandler)
266
+ registerPublicTool(listDataSourcesName, withKnowledgeFirstRequirement(listDataSourcesDescription), listDataSourcesSchema, listDataSourcesHandler)
267
+ registerPublicTool(listSkillsName, listSkillsDescription, listSkillsSchema, listSkillsHandler)
268
+ registerPublicTool(loadSkillName, loadSkillDescription, loadSkillSchema, loadSkillHandler)
269
+ registerPublicTool(extractPostEngagementName, withKnowledgeFirstRequirement(extractPostEngagementDescription), extractPostEngagementSchema, extractPostEngagementHandler)
270
+ registerPublicTool(getPlaceReviewsName, withKnowledgeFirstRequirement(getPlaceReviewsDescription), getPlaceReviewsSchema, getPlaceReviewsHandler)
257
271
  // ColdIQ Visitor ID: read the leads identified on the account's own websites.
258
272
  // Listing the websites themselves needs no tool — search_endpoints/call_endpoint
259
273
  // already reach GET /visitor-id/websites.
260
- server.tool(getWebsiteVisitorsName, withKnowledgeFirstRequirement(getWebsiteVisitorsDescription), getWebsiteVisitorsSchema, getWebsiteVisitorsHandler)
274
+ registerPublicTool(getWebsiteVisitorsName, withKnowledgeFirstRequirement(getWebsiteVisitorsDescription), getWebsiteVisitorsSchema, getWebsiteVisitorsHandler)
261
275
  // Generic endpoint discovery + invoke: reach any integrated provider without a dedicated tool.
262
- server.tool(searchEndpointsName, withKnowledgeFirstRequirement(searchEndpointsDescription), searchEndpointsSchema, searchEndpointsHandler)
263
- server.tool(getEndpointDetailsName, withKnowledgeFirstRequirement(getEndpointDetailsDescription), getEndpointDetailsSchema, getEndpointDetailsHandler)
264
- server.tool(callEndpointName, withKnowledgeFirstRequirement(callEndpointDescription), callEndpointSchema, callEndpointHandler)
276
+ registerPublicTool(searchEndpointsName, withKnowledgeFirstRequirement(searchEndpointsDescription), searchEndpointsSchema, searchEndpointsHandler)
277
+ registerPublicTool(getEndpointDetailsName, withKnowledgeFirstRequirement(getEndpointDetailsDescription), getEndpointDetailsSchema, getEndpointDetailsHandler)
278
+ registerPublicTool(callEndpointName, withKnowledgeFirstRequirement(callEndpointDescription), callEndpointSchema, callEndpointHandler)
265
279
 
266
280
  // Connect via stdio transport
267
281
  const transport = new StdioServerTransport()
@@ -0,0 +1,21 @@
1
+ import { z } from 'zod'
2
+
3
+ const ToolInputJsonSchema = z.object({
4
+ type: z.literal('object'),
5
+ properties: z.json().optional(),
6
+ }).passthrough()
7
+
8
+ /** Convert a tool input schema without widening Zod object contracts. */
9
+ export function toOpenApiInputSchema(schema: z.ZodObject) {
10
+ const generated = z.toJSONSchema(schema, {
11
+ target: 'openapi-3.0',
12
+ io: 'input',
13
+ override: ({ zodSchema, jsonSchema }) => {
14
+ if (zodSchema instanceof z.ZodObject && jsonSchema.additionalProperties === undefined) {
15
+ jsonSchema.additionalProperties = false
16
+ }
17
+ },
18
+ })
19
+
20
+ return ToolInputJsonSchema.parse(generated)
21
+ }
@@ -16,7 +16,7 @@ export const callEndpointSchema = {
16
16
  method: z.enum(['GET', 'POST', 'PUT', 'PATCH', 'DELETE']).optional()
17
17
  .describe('HTTP method from search_endpoints. Set this when one path supports more than one method.'),
18
18
  params: z
19
- .record(z.unknown())
19
+ .record(z.string(), z.unknown())
20
20
  .optional()
21
21
  .describe('Provider-native request params (request body fields, plus any path/query params), as shown in get_endpoint_details.'),
22
22
  }
@@ -13,7 +13,7 @@ export const findInfluencersSchema = {
13
13
  .describe('Target platform.'),
14
14
  limit: z.number().int().min(1).max(100).default(25)
15
15
  .describe('Max creators to return (1–100). Pricing is returned in the response credit headers.'),
16
- page: z.number().int().min(1).default(1).optional()
16
+ page: z.number().int().min(1).optional().meta({ default: 1 })
17
17
  .describe('Page number for pagination.'),
18
18
  sort_by: z.enum(['relevancy', 'number_of_followers', 'engagement_rate']).optional(),
19
19
  sort_order: z.enum(['asc', 'desc']).optional(),
@@ -26,8 +26,8 @@ export const searchKnowledgeSchema = {
26
26
  .int()
27
27
  .min(1)
28
28
  .max(15)
29
- .default(5)
30
29
  .optional()
30
+ .meta({ default: 5 })
31
31
  .describe('Maximum ranked documents to return (default 5, max 15).'),
32
32
  }
33
33
 
@@ -1,10 +1,10 @@
1
1
  import Anthropic from '@anthropic-ai/sdk'
2
- import { zodToJsonSchema } from 'zod-to-json-schema'
3
2
  import * as fs from 'node:fs'
4
3
  import * as path from 'node:path'
5
4
  import * as url from 'node:url'
6
5
 
7
6
  import { MODEL_PROFILES } from '../../../src/agent/model-policy.js'
7
+ import { toOpenApiInputSchema } from '../../src/tool-json-schema.js'
8
8
  import { MCP_TOOLS } from './tools-bridge.js'
9
9
 
10
10
  export interface ToolCall {
@@ -49,7 +49,7 @@ export async function runAgent(options: RunAgentOptions): Promise<AgentResult> {
49
49
  const anthropicTools: Anthropic.Tool[] = MCP_TOOLS.map((t) => ({
50
50
  name: t.name,
51
51
  description: t.description,
52
- input_schema: zodToJsonSchema(t.schema, { target: 'openApi3' }) as Anthropic.Tool['input_schema'],
52
+ input_schema: toOpenApiInputSchema(t.schema),
53
53
  }))
54
54
 
55
55
  const messages: Anthropic.MessageParam[] = [{ role: 'user', content: prompt }]
@@ -50,7 +50,7 @@ describe('MCP knowledge-first policy', () => {
50
50
  const indexPath = fileURLToPath(new URL('../src/index.ts', import.meta.url))
51
51
  const registrations = readFileSync(indexPath, 'utf8')
52
52
  .split('\n')
53
- .filter((line) => line.trimStart().startsWith('server.tool('))
53
+ .filter((line) => line.trimStart().startsWith('registerPublicTool('))
54
54
  const unguardedCompatibilityTools = [
55
55
  'getCreditBalanceName',
56
56
  'getSessionUsageName',
@@ -58,9 +58,9 @@ describe('MCP knowledge-first policy', () => {
58
58
  'loadSkillName',
59
59
  ]
60
60
 
61
- expect(registrations[0]).toContain('server.tool(searchKnowledgeName, searchKnowledgeDescription')
61
+ expect(registrations[0]).toContain('registerPublicTool(searchKnowledgeName, searchKnowledgeDescription')
62
62
  for (const registration of registrations.slice(1)) {
63
- if (unguardedCompatibilityTools.some((name) => registration.includes(`server.tool(${name},`))) continue
63
+ if (unguardedCompatibilityTools.some((name) => registration.includes(`registerPublicTool(${name},`))) continue
64
64
  expect(registration).toContain('withKnowledgeFirstRequirement(')
65
65
  }
66
66
  })
@@ -0,0 +1,59 @@
1
+ import { Client } from '@modelcontextprotocol/sdk/client/index.js'
2
+ import { InMemoryTransport } from '@modelcontextprotocol/sdk/inMemory.js'
3
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
4
+ import { afterEach, describe, expect, it } from 'vitest'
5
+ import { z } from 'zod'
6
+
7
+ describe('MCP SDK Zod 4 compatibility', () => {
8
+ const closeCallbacks: Array<() => Promise<void>> = []
9
+
10
+ afterEach(async () => {
11
+ await Promise.all(closeCallbacks.splice(0).map((close) => close()))
12
+ })
13
+
14
+ it('publishes populated input properties and required fields over the wire', async () => {
15
+ const server = new McpServer({ name: 'schema-contract-server', version: '1.0.0' })
16
+ server.registerTool(
17
+ 'schema_contract',
18
+ {
19
+ description: 'Verify Zod 4 tool schema conversion.',
20
+ inputSchema: z.strictObject({
21
+ platform: z.enum(['linkedin', 'instagram']),
22
+ limit: z.number().int().min(1).max(100).default(25),
23
+ }),
24
+ },
25
+ async () => ({ content: [{ type: 'text', text: 'ok' }] }),
26
+ )
27
+
28
+ const client = new Client({ name: 'schema-contract-client', version: '1.0.0' })
29
+ const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair()
30
+ closeCallbacks.push(() => client.close(), () => server.close())
31
+
32
+ await server.connect(serverTransport)
33
+ await client.connect(clientTransport)
34
+
35
+ const result = await client.listTools()
36
+ expect(result.tools).toHaveLength(1)
37
+ expect(result.tools[0]?.inputSchema).toMatchObject({
38
+ type: 'object',
39
+ properties: {
40
+ platform: { type: 'string', enum: ['linkedin', 'instagram'] },
41
+ limit: { type: 'integer', minimum: 1, maximum: 100, default: 25 },
42
+ },
43
+ required: ['platform'],
44
+ additionalProperties: false,
45
+ })
46
+
47
+ const invalidCall = await client.callTool({
48
+ name: 'schema_contract',
49
+ arguments: { platform: 'linkedin', unexpected: true },
50
+ })
51
+ expect(invalidCall.isError).toBe(true)
52
+ expect(invalidCall.content).toEqual([
53
+ expect.objectContaining({
54
+ type: 'text',
55
+ text: expect.stringContaining('Unrecognized key: "unexpected"'),
56
+ }),
57
+ ])
58
+ })
59
+ })
@@ -0,0 +1,94 @@
1
+ import { readdirSync, readFileSync } from 'node:fs'
2
+ import { describe, expect, it } from 'vitest'
3
+ import { z } from 'zod'
4
+
5
+ import { toOpenApiInputSchema } from '../src/tool-json-schema.js'
6
+ import { MCP_TOOLS } from './gtm/tools-bridge.js'
7
+
8
+ const PublicToolSchema = z.record(z.string(), z.instanceof(z.ZodType))
9
+
10
+ describe('public MCP tool JSON schemas', () => {
11
+ it('converts every registered public tool schema', async () => {
12
+ const toolFiles = readdirSync(new URL('../src/tools/', import.meta.url))
13
+ .filter((file) => file.endsWith('.ts'))
14
+ .sort()
15
+ const indexSource = readFileSync(new URL('../src/index.ts', import.meta.url), 'utf8')
16
+
17
+ expect(indexSource.match(/registerPublicTool\(/g)).toHaveLength(toolFiles.length)
18
+ expect(indexSource).not.toContain('server.tool(')
19
+
20
+ for (const file of toolFiles) {
21
+ const exports = await import(new URL(`../src/tools/${file}`, import.meta.url).href)
22
+ const schemaEntries = Object.entries(exports).filter(([name]) => name.endsWith('Schema'))
23
+ const nameEntries = Object.entries(exports).filter(([name]) => name.endsWith('Name'))
24
+
25
+ expect(schemaEntries, file).toHaveLength(1)
26
+ expect(nameEntries, file).toHaveLength(1)
27
+
28
+ const schema = toOpenApiInputSchema(z.object(PublicToolSchema.parse(schemaEntries[0][1])))
29
+ expect(schema, z.string().parse(nameEntries[0][1])).toMatchObject({
30
+ type: 'object',
31
+ additionalProperties: false,
32
+ properties: expect.any(Object),
33
+ })
34
+ }
35
+ })
36
+
37
+ it('converts every tool schema without widening object inputs', () => {
38
+ for (const tool of MCP_TOOLS) {
39
+ const schema = toOpenApiInputSchema(tool.schema)
40
+
41
+ expect(schema, tool.name).toMatchObject({
42
+ type: 'object',
43
+ additionalProperties: false,
44
+ properties: expect.any(Object),
45
+ })
46
+ expect(schema, tool.name).not.toHaveProperty('$schema')
47
+ }
48
+ })
49
+
50
+ it('closes only Zod objects and does not change record or default data', () => {
51
+ const schema = toOpenApiInputSchema(z.object({
52
+ nested: z.object({ value: z.string() }),
53
+ labels: z.record(z.string(), z.string()),
54
+ settings: z.object({ type: z.literal('object') }).default({ type: 'object' }),
55
+ }))
56
+
57
+ expect(schema).toMatchObject({
58
+ properties: {
59
+ nested: { additionalProperties: false },
60
+ labels: { additionalProperties: { type: 'string' } },
61
+ settings: {
62
+ additionalProperties: false,
63
+ default: { type: 'object' },
64
+ },
65
+ },
66
+ })
67
+ })
68
+
69
+ it('preserves required fields, defaults, limits, and optional defaults', () => {
70
+ const tool = MCP_TOOLS.find((candidate) => candidate.name === 'find_influencers')
71
+ expect(tool).toBeDefined()
72
+ if (!tool) throw new Error('find_influencers is not registered')
73
+
74
+ const schema = toOpenApiInputSchema(tool.schema)
75
+
76
+ expect(schema.required).toEqual(['platform'])
77
+ expect(schema).toMatchObject({
78
+ properties: {
79
+ limit: {
80
+ type: 'integer',
81
+ minimum: 1,
82
+ maximum: 100,
83
+ default: 25,
84
+ },
85
+ page: {
86
+ type: 'integer',
87
+ minimum: 1,
88
+ default: 1,
89
+ },
90
+ },
91
+ })
92
+ expect(schema.properties?.page).not.toHaveProperty('nullable')
93
+ })
94
+ })