@breaknorm_hu/mcp-server 0.1.0 → 0.1.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.
Files changed (2) hide show
  1. package/dist/index.js +94 -184
  2. package/package.json +3 -2
package/dist/index.js CHANGED
@@ -10,7 +10,7 @@ var BreakNormClient = class {
10
10
  apiKey;
11
11
  constructor(apiKey, baseUrl) {
12
12
  this.apiKey = apiKey;
13
- this.baseUrl = (baseUrl || "https://breaknorm.com").replace(/\/$/, "");
13
+ this.baseUrl = (baseUrl || "https://breaknorm.hu").replace(/\/$/, "");
14
14
  }
15
15
  async request(method, path, options) {
16
16
  const url = new URL(`/api/v1${path}`, this.baseUrl);
@@ -49,6 +49,7 @@ var BreakNormClient = class {
49
49
  };
50
50
 
51
51
  // src/tools.ts
52
+ import { z } from "zod";
52
53
  function arrayParam(val) {
53
54
  if (Array.isArray(val) && val.length > 0) return val.join(",");
54
55
  return void 0;
@@ -58,55 +59,32 @@ function str(val) {
58
59
  return String(val);
59
60
  }
60
61
  var tools = [
61
- // ── Search ─────────────────────────────────
62
62
  {
63
63
  name: "search_contacts",
64
64
  description: "Search the Breaknorm contact database. Returns contacts matching filters. Use get_search_filters first to discover available filter values. Results paginated (max 25/page). For page 2+, pass searchToken from page 1.",
65
- inputSchema: {
66
- type: "object",
67
- properties: {
68
- q: { type: "string", description: "Free text search (name, title, company)" },
69
- industry: { type: "array", items: { type: "string" }, description: "Filter by industry names" },
70
- city: { type: "array", items: { type: "string" }, description: "Filter by city names" },
71
- county: { type: "array", items: { type: "string" }, description: "Filter by county (megye)" },
72
- country: { type: "string", description: "2-letter country code (default: HU)" },
73
- seniority: {
74
- type: "array",
75
- items: { type: "string", enum: ["c_level", "vp", "director", "manager", "head", "senior", "entry", "intern", "founder", "partner", "managing_director", "individual_contributor"] },
76
- description: "Filter by seniority level"
77
- },
78
- department: {
79
- type: "array",
80
- items: { type: "string", enum: ["sales", "marketing", "it", "hr", "finance", "operations", "legal", "engineering", "product", "customer_success", "c_suite", "design", "other"] }
81
- },
82
- employee_min: { type: "integer", description: "Minimum company employee count" },
83
- employee_max: { type: "integer", description: "Maximum company employee count" },
84
- revenue_min: { type: "number", description: "Minimum annual revenue (HUF)" },
85
- revenue_max: { type: "number", description: "Maximum annual revenue (HUF)" },
86
- has_email: { type: "string", enum: ["yes", "maybe", "no"], description: "Filter by email availability" },
87
- has_phone: { type: "string", enum: ["yes", "maybe", "no"] },
88
- title: { type: "string", description: "Job title keyword search" },
89
- page: { type: "integer", default: 1 },
90
- limit: { type: "integer", default: 25, maximum: 25 },
91
- searchToken: { type: "string", description: "Required for page 2+. From page 1 response." }
92
- }
93
- },
65
+ schema: z.object({
66
+ q: z.string().optional().describe("Free text search (name, title, company)"),
67
+ industry: z.array(z.string()).optional().describe("Filter by industry names"),
68
+ city: z.array(z.string()).optional().describe("Filter by city names"),
69
+ seniority: z.array(z.string()).optional().describe("Filter by seniority: c_level, vp, director, manager, head, senior, founder, partner"),
70
+ department: z.array(z.string()).optional().describe("Filter by department: sales, marketing, it, hr, finance, engineering, c_suite"),
71
+ employee_min: z.number().int().optional().describe("Min company employees"),
72
+ employee_max: z.number().int().optional().describe("Max company employees"),
73
+ has_email: z.enum(["yes", "maybe", "no"]).optional().describe("Filter by email availability"),
74
+ page: z.number().int().default(1).describe("Page number"),
75
+ limit: z.number().int().max(25).default(25).describe("Results per page (max 25)"),
76
+ searchToken: z.string().optional().describe("Required for page 2+, from page 1 response")
77
+ }),
94
78
  handler: async (client, args) => {
95
79
  return client.get("/contacts/search", {
96
80
  q: str(args.q),
97
81
  industry: arrayParam(args.industry),
98
82
  city: arrayParam(args.city),
99
- county: arrayParam(args.county),
100
- country: str(args.country),
101
83
  seniority: arrayParam(args.seniority),
102
84
  department: arrayParam(args.department),
103
85
  employee_min: str(args.employee_min),
104
86
  employee_max: str(args.employee_max),
105
- revenue_min: str(args.revenue_min),
106
- revenue_max: str(args.revenue_max),
107
87
  has_email: str(args.has_email),
108
- has_phone: str(args.has_phone),
109
- title: str(args.title),
110
88
  page: str(args.page),
111
89
  limit: str(args.limit),
112
90
  searchToken: str(args.searchToken)
@@ -115,37 +93,26 @@ var tools = [
115
93
  },
116
94
  {
117
95
  name: "search_companies",
118
- description: "Search companies in the Breaknorm database. Returns companies matching filters.",
119
- inputSchema: {
120
- type: "object",
121
- properties: {
122
- q: { type: "string", description: "Free text company name search" },
123
- industry: { type: "array", items: { type: "string" } },
124
- city: { type: "array", items: { type: "string" } },
125
- county: { type: "array", items: { type: "string" } },
126
- country: { type: "string" },
127
- employee_min: { type: "integer" },
128
- employee_max: { type: "integer" },
129
- revenue_min: { type: "number" },
130
- revenue_max: { type: "number" },
131
- has_website: { type: "boolean" },
132
- page: { type: "integer", default: 1 },
133
- limit: { type: "integer", default: 25, maximum: 25 },
134
- sort: { type: "string", enum: ["relevance", "name", "employee_count", "revenue", "icp_score"], default: "relevance" },
135
- searchToken: { type: "string" }
136
- }
137
- },
96
+ description: "Search companies in the Breaknorm database.",
97
+ schema: z.object({
98
+ q: z.string().optional().describe("Company name search"),
99
+ industry: z.array(z.string()).optional(),
100
+ city: z.array(z.string()).optional(),
101
+ employee_min: z.number().int().optional(),
102
+ employee_max: z.number().int().optional(),
103
+ has_website: z.boolean().optional(),
104
+ page: z.number().int().default(1),
105
+ limit: z.number().int().max(25).default(25),
106
+ sort: z.enum(["relevance", "name", "employee_count", "revenue", "icp_score"]).default("relevance"),
107
+ searchToken: z.string().optional()
108
+ }),
138
109
  handler: async (client, args) => {
139
110
  return client.get("/companies/search", {
140
111
  q: str(args.q),
141
112
  industry: arrayParam(args.industry),
142
113
  city: arrayParam(args.city),
143
- county: arrayParam(args.county),
144
- country: str(args.country),
145
114
  employee_min: str(args.employee_min),
146
115
  employee_max: str(args.employee_max),
147
- revenue_min: str(args.revenue_min),
148
- revenue_max: str(args.revenue_max),
149
116
  has_website: str(args.has_website),
150
117
  page: str(args.page),
151
118
  limit: str(args.limit),
@@ -156,172 +123,119 @@ var tools = [
156
123
  },
157
124
  {
158
125
  name: "get_search_filters",
159
- description: "Get all available filter values with counts (industries, cities, counties, seniorities, departments, etc.). Call this FIRST before searching to understand what filter values are available.",
160
- inputSchema: { type: "object", properties: {} },
126
+ description: "Get all available filter values with counts (industries, cities, seniorities, departments). Call this FIRST before searching.",
127
+ schema: z.object({}),
161
128
  handler: async (client) => client.get("/search/filters")
162
129
  },
163
- // ── Contacts ───────────────────────────────
164
130
  {
165
131
  name: "get_contact",
166
132
  description: "Get details of a single contact (public fields, no revealed data).",
167
- inputSchema: {
168
- type: "object",
169
- properties: { contactId: { type: "string", description: "Contact UUID" } },
170
- required: ["contactId"]
171
- },
133
+ schema: z.object({
134
+ contactId: z.string().describe("Contact UUID")
135
+ }),
172
136
  handler: async (client, args) => client.get(`/contacts/${args.contactId}`)
173
137
  },
174
- // ── Cost Estimation ────────────────────────
175
138
  {
176
139
  name: "estimate_cost",
177
- description: "IMPORTANT: Call this BEFORE any paid operation (reveal, ICP scoring). Returns exact credit cost, current balance, and whether you can afford it. Present the cost to the user for approval before proceeding.",
178
- inputSchema: {
179
- type: "object",
180
- properties: {
181
- operations: {
182
- type: "array",
183
- items: {
184
- type: "object",
185
- properties: {
186
- type: { type: "string", enum: ["reveal", "icp_score"] },
187
- contactIds: { type: "array", items: { type: "string" }, description: "For reveal operations" },
188
- revealType: { type: "string", enum: ["email", "phone", "all"], description: "For reveal: what to reveal" },
189
- companyIds: { type: "array", items: { type: "string" }, description: "For icp_score operations" },
190
- icpProfileId: { type: "string", description: "For icp_score: specific profile ID (optional)" }
191
- },
192
- required: ["type"]
193
- },
194
- description: "List of operations to estimate"
195
- }
196
- },
197
- required: ["operations"]
198
- },
140
+ description: "IMPORTANT: Call BEFORE any paid operation (reveal, ICP scoring). Returns exact credit cost, balance, and whether you can afford it. Present cost to user for approval.",
141
+ schema: z.object({
142
+ operations: z.array(z.object({
143
+ type: z.enum(["reveal", "icp_score"]),
144
+ contactIds: z.array(z.string()).optional().describe("For reveal operations"),
145
+ revealType: z.enum(["email", "phone", "all"]).optional().describe("For reveal: what to reveal"),
146
+ companyIds: z.array(z.string()).optional().describe("For icp_score operations"),
147
+ icpProfileId: z.string().optional().describe("For icp_score: specific profile")
148
+ })).describe("List of operations to estimate")
149
+ }),
199
150
  handler: async (client, args) => client.post("/estimate", { operations: args.operations })
200
151
  },
201
- // ── Reveal ─────────────────────────────────
202
152
  {
203
153
  name: "reveal_contact",
204
- description: "Reveal email and/or phone for a single contact. Costs credits (email=1, phone=10, all=11). ALWAYS call estimate_cost first and get user approval.",
205
- inputSchema: {
206
- type: "object",
207
- properties: {
208
- contactId: { type: "string", description: "Contact UUID" },
209
- type: { type: "string", enum: ["email", "phone", "all"], description: "What to reveal" }
210
- },
211
- required: ["contactId", "type"]
212
- },
154
+ description: "Reveal email/phone for a single contact. Costs credits (email=1, phone=10, all=11). ALWAYS call estimate_cost first.",
155
+ schema: z.object({
156
+ contactId: z.string().describe("Contact UUID"),
157
+ type: z.enum(["email", "phone", "all"]).describe("What to reveal")
158
+ }),
213
159
  handler: async (client, args) => client.post(`/contacts/${args.contactId}/reveal`, { type: args.type })
214
160
  },
215
161
  {
216
162
  name: "bulk_reveal_contacts",
217
- description: "Reveal data for multiple contacts at once. Creates an async job. Returns jobId \u2014 use check_job_status to poll progress. ALWAYS call estimate_cost first.",
218
- inputSchema: {
219
- type: "object",
220
- properties: {
221
- contactIds: { type: "array", items: { type: "string" }, description: "Contact UUIDs (max 1000)" },
222
- type: { type: "string", enum: ["email", "phone", "all"] },
223
- idempotencyKey: { type: "string", description: "UUID to prevent duplicate charges on retry" }
224
- },
225
- required: ["contactIds", "type"]
226
- },
163
+ description: "Reveal data for multiple contacts at once (async job). Returns jobId. ALWAYS call estimate_cost first.",
164
+ schema: z.object({
165
+ contactIds: z.array(z.string()).describe("Contact UUIDs (max 1000)"),
166
+ type: z.enum(["email", "phone", "all"]),
167
+ idempotencyKey: z.string().optional().describe("UUID to prevent double charges on retry")
168
+ }),
227
169
  handler: async (client, args) => client.post("/contacts/bulk-reveal", {
228
170
  contactIds: args.contactIds,
229
171
  type: args.type,
230
172
  idempotencyKey: args.idempotencyKey
231
173
  })
232
174
  },
233
- // ── Jobs ───────────────────────────────────
234
175
  {
235
176
  name: "check_job_status",
236
- description: "Check the status of an async job (bulk reveal, export, ICP scoring). Returns progress (processedItems/totalItems) and status (pending/processing/completed/failed).",
237
- inputSchema: {
238
- type: "object",
239
- properties: { jobId: { type: "string", description: "Job UUID" } },
240
- required: ["jobId"]
241
- },
177
+ description: "Check async job status (bulk reveal, export, ICP scoring). Returns progress and status.",
178
+ schema: z.object({
179
+ jobId: z.string().describe("Job UUID")
180
+ }),
242
181
  handler: async (client, args) => client.get(`/jobs/${args.jobId}`)
243
182
  },
244
- // ── Credits ────────────────────────────────
245
183
  {
246
184
  name: "get_credits",
247
185
  description: "Check current credit balance and subscription tier.",
248
- inputSchema: { type: "object", properties: {} },
186
+ schema: z.object({}),
249
187
  handler: async (client) => client.get("/credits")
250
188
  },
251
- // ── Lists ──────────────────────────────────
252
189
  {
253
190
  name: "list_saved_lists",
254
191
  description: "List all saved contact lists.",
255
- inputSchema: {
256
- type: "object",
257
- properties: {
258
- page: { type: "integer", default: 1 },
259
- limit: { type: "integer", default: 20, maximum: 100 },
260
- sort: { type: "string", enum: ["name", "created_at", "contact_count"], default: "created_at" }
261
- }
262
- },
192
+ schema: z.object({
193
+ page: z.number().int().default(1).optional(),
194
+ limit: z.number().int().max(100).default(20).optional(),
195
+ sort: z.enum(["name", "created_at", "contact_count"]).default("created_at").optional()
196
+ }),
263
197
  handler: async (client, args) => client.get("/lists", { page: str(args.page), limit: str(args.limit), sort: str(args.sort) })
264
198
  },
265
199
  {
266
200
  name: "create_list",
267
201
  description: "Create a new saved contact list.",
268
- inputSchema: {
269
- type: "object",
270
- properties: {
271
- name: { type: "string", description: "List name (1-200 chars)" },
272
- description: { type: "string", description: "Optional description (max 1000 chars)" }
273
- },
274
- required: ["name"]
275
- },
202
+ schema: z.object({
203
+ name: z.string().describe("List name (1-200 chars)"),
204
+ description: z.string().optional().describe("Optional description")
205
+ }),
276
206
  handler: async (client, args) => client.post("/lists", { name: args.name, description: args.description })
277
207
  },
278
208
  {
279
209
  name: "add_contacts_to_list",
280
- description: "Add contacts to an existing list. Duplicates are automatically skipped.",
281
- inputSchema: {
282
- type: "object",
283
- properties: {
284
- listId: { type: "string", description: "List UUID" },
285
- contactIds: { type: "array", items: { type: "string" }, description: "Contact UUIDs (max 500)" }
286
- },
287
- required: ["listId", "contactIds"]
288
- },
210
+ description: "Add contacts to a list. Duplicates auto-skipped.",
211
+ schema: z.object({
212
+ listId: z.string().describe("List UUID"),
213
+ contactIds: z.array(z.string()).describe("Contact UUIDs (max 500)")
214
+ }),
289
215
  handler: async (client, args) => client.post(`/lists/${args.listId}/contacts`, { contactIds: args.contactIds })
290
216
  },
291
217
  {
292
218
  name: "export_list",
293
- description: "Export a saved list as CSV. Creates async job \u2014 use check_job_status to poll.",
294
- inputSchema: {
295
- type: "object",
296
- properties: {
297
- listId: { type: "string", description: "List UUID" }
298
- },
299
- required: ["listId"]
300
- },
219
+ description: "Export a list as CSV (async job). Use check_job_status to poll.",
220
+ schema: z.object({
221
+ listId: z.string().describe("List UUID")
222
+ }),
301
223
  handler: async (client, args) => client.post(`/lists/${args.listId}/export`)
302
224
  },
303
- // ── ICP ────────────────────────────────────
304
225
  {
305
226
  name: "list_icp_profiles",
306
- description: "List user's ICP (Ideal Customer Profile) definitions.",
307
- inputSchema: { type: "object", properties: {} },
227
+ description: "List ICP (Ideal Customer Profile) definitions.",
228
+ schema: z.object({}),
308
229
  handler: async (client) => client.get("/icp-profiles")
309
230
  },
310
231
  {
311
232
  name: "create_icp_profile",
312
- description: "Create an ICP profile from a natural language description. This defines what your ideal customer looks like, used for AI scoring.",
313
- inputSchema: {
314
- type: "object",
315
- properties: {
316
- name: { type: "string", description: "Profile name (e.g., 'SaaS c\xE9gek Budapest')" },
317
- description: {
318
- type: "string",
319
- description: "Detailed ICP description in natural language (1-2000 chars). Example: 'IT \xE9s SaaS c\xE9gek, 10-200 alkalmazott, Budapest, \xE9ves bev\xE9tel 100M-1B HUF, akt\xEDv LinkedIn jelenl\xE9t'"
320
- },
321
- isDefault: { type: "boolean", description: "Set as default profile for scoring", default: false }
322
- },
323
- required: ["name", "description"]
324
- },
233
+ description: "Create an ICP profile from natural language description for AI scoring.",
234
+ schema: z.object({
235
+ name: z.string().describe("Profile name"),
236
+ description: z.string().describe("Detailed ICP description (1-2000 chars)"),
237
+ isDefault: z.boolean().default(false).optional()
238
+ }),
325
239
  handler: async (client, args) => client.post("/icp-profiles", {
326
240
  name: args.name,
327
241
  description: args.description,
@@ -330,16 +244,12 @@ var tools = [
330
244
  },
331
245
  {
332
246
  name: "score_companies",
333
- description: "Score companies against an ICP profile using AI. Creates async job. Costs 1 credit per company. ALWAYS call estimate_cost first.",
334
- inputSchema: {
335
- type: "object",
336
- properties: {
337
- companyIds: { type: "array", items: { type: "string" }, description: "Company UUIDs (max 500)" },
338
- icpProfileId: { type: "string", description: "ICP profile UUID (uses default if omitted)" },
339
- idempotencyKey: { type: "string", description: "UUID to prevent duplicate charges" }
340
- },
341
- required: ["companyIds"]
342
- },
247
+ description: "Score companies against ICP profile using AI (async). 1 credit/company. ALWAYS call estimate_cost first.",
248
+ schema: z.object({
249
+ companyIds: z.array(z.string()).describe("Company UUIDs (max 500)"),
250
+ icpProfileId: z.string().optional().describe("ICP profile UUID (uses default if omitted)"),
251
+ idempotencyKey: z.string().optional()
252
+ }),
343
253
  handler: async (client, args) => client.post("/icp-scores/bulk", {
344
254
  companyIds: args.companyIds,
345
255
  icpProfileId: args.icpProfileId,
@@ -384,10 +294,10 @@ async function main() {
384
294
  const apiKey = process.env.BREAKNORM_API_KEY;
385
295
  if (!apiKey) {
386
296
  console.error("Error: BREAKNORM_API_KEY environment variable is required.");
387
- console.error("Get your API key at https://breaknorm.com/app/settings/developers");
297
+ console.error("Get your API key at https://breaknorm.hu/app/settings/developers");
388
298
  process.exit(1);
389
299
  }
390
- const baseUrl = process.env.BREAKNORM_BASE_URL || "https://breaknorm.com";
300
+ const baseUrl = process.env.BREAKNORM_BASE_URL || "https://breaknorm.hu";
391
301
  const client = new BreakNormClient(apiKey, baseUrl);
392
302
  const server = new McpServer({
393
303
  name: SERVER_NAME,
@@ -399,7 +309,7 @@ async function main() {
399
309
  server.tool(
400
310
  tool.name,
401
311
  tool.description,
402
- tool.inputSchema,
312
+ tool.schema.shape,
403
313
  async (args) => {
404
314
  try {
405
315
  const result = await tool.handler(client, args);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@breaknorm_hu/mcp-server",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Breaknorm MCP Server — AI agent interface for B2B contact database",
5
5
  "type": "module",
6
6
  "bin": {
@@ -13,7 +13,8 @@
13
13
  "start": "node dist/index.js"
14
14
  },
15
15
  "dependencies": {
16
- "@modelcontextprotocol/sdk": "^1.12.1"
16
+ "@modelcontextprotocol/sdk": "^1.12.1",
17
+ "zod": "^4.3.6"
17
18
  },
18
19
  "devDependencies": {
19
20
  "tsup": "^8.4.0",