llmconveyors-mcp 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.
Files changed (2) hide show
  1. package/dist/index.js +259 -207
  2. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -9,7 +9,7 @@ import { LLMConveyors } from "llmconveyors";
9
9
  import { z } from "zod";
10
10
 
11
11
  // src/utils/error-handler.ts
12
- import { LLMConveyorsError, RateLimitError } from "llmconveyors";
12
+ import { LLMConveyorsError, RateLimitError, NetworkError, TimeoutError } from "llmconveyors";
13
13
  function handleToolError(err) {
14
14
  if (err instanceof LLMConveyorsError) {
15
15
  const payload = {
@@ -19,10 +19,13 @@ function handleToolError(err) {
19
19
  ...err.hint != null && { hint: err.hint },
20
20
  ...err.details != null && { details: err.details },
21
21
  ...err.requestId != null && { requestId: err.requestId },
22
+ ...err.timestamp != null && { timestamp: err.timestamp },
23
+ ...err.path != null && { path: err.path },
22
24
  retryable: err.isRetryable()
23
25
  };
24
26
  if (err instanceof RateLimitError) {
25
- if (err.retryAfter != null) payload.retryAfterMs = err.retryAfter;
27
+ if (err.retryAfter != null) payload.retryAfterSeconds = err.retryAfter;
28
+ if (err.retryAfterMs != null) payload.retryAfterMs = err.retryAfterMs;
26
29
  if (err.rateLimitInfo != null) payload.rateLimitInfo = err.rateLimitInfo;
27
30
  }
28
31
  return {
@@ -30,6 +33,28 @@ function handleToolError(err) {
30
33
  isError: true
31
34
  };
32
35
  }
36
+ if (err instanceof NetworkError) {
37
+ return {
38
+ content: [{ type: "text", text: JSON.stringify({
39
+ error: err.message,
40
+ type: "NetworkError",
41
+ retryable: true,
42
+ ...err.cause != null && { cause: String(err.cause) }
43
+ }, null, 2) }],
44
+ isError: true
45
+ };
46
+ }
47
+ if (err instanceof TimeoutError) {
48
+ return {
49
+ content: [{ type: "text", text: JSON.stringify({
50
+ error: err.message,
51
+ type: "TimeoutError",
52
+ retryable: true,
53
+ ...err.cause != null && { cause: String(err.cause) }
54
+ }, null, 2) }],
55
+ isError: true
56
+ };
57
+ }
33
58
  const message = err instanceof Error ? err.message : String(err);
34
59
  return {
35
60
  content: [{ type: "text", text: `Error: ${message}` }],
@@ -41,23 +66,35 @@ function handleToolError(err) {
41
66
  function registerAgentTools(server2, client2) {
42
67
  server2.tool(
43
68
  "job-hunter-run",
44
- "Run the Job Hunter agent \u2014 generates a tailored CV, cover letter, and cold email for a job application. Returns artifacts when complete. Consumes 30-500 credits. Rate limited: 10 req/min. Requires scope: jobs:write. Check balance with settings-usage-summary before running.",
69
+ "Run the Job Hunter agent \u2014 generates a tailored CV, cover letter, and cold email for a job application. Supports phased execution: set autoSelectContacts=false to pause for contact selection (poll with agent-status, respond with agent-interact). Consumes 30-500 credits. Rate limited: 10 req/min. Requires scope: jobs:write. Check balance with settings-usage-summary before running.",
45
70
  {
46
71
  companyName: z.string().describe("Target company name"),
47
72
  jobTitle: z.string().describe("Job title to apply for"),
48
- jobDescription: z.string().optional().describe("Full job description text"),
49
- companyWebsite: z.string().optional().describe("Target company website URL for research phase"),
73
+ jobDescription: z.string().describe("Full job description text"),
74
+ companyWebsite: z.string().describe("Target company website URL for research phase"),
75
+ sessionId: z.string().optional().describe("Existing session ID to continue a previous run"),
76
+ generationId: z.string().optional().describe("Existing generation ID to continue"),
50
77
  masterResumeId: z.string().optional().describe("ID of a stored master resume to use"),
78
+ tier: z.enum(["free", "byo"]).optional().describe("Billing tier: free (platform credits) or byo (bring your own key)"),
79
+ model: z.enum(["flash", "pro"]).optional().describe("AI model: flash (faster/cheaper) or pro (higher quality)"),
80
+ webhookUrl: z.string().optional().describe("Webhook URL for async status updates (generation.completed, generation.failed, generation.awaiting_input)"),
81
+ mode: z.enum(["standard", "cold_outreach"]).optional().describe("Generation mode"),
51
82
  theme: z.enum(["even", "stackoverflow", "class", "professional", "elegant", "macchiato", "react", "academic"]).optional().describe("Resume theme"),
83
+ autoSelectContacts: z.boolean().optional().describe("Set false for phased execution with contact selection gate. Default true for API keys."),
84
+ skipResearchCache: z.boolean().optional().describe("Force fresh company research instead of using cache"),
52
85
  contactName: z.string().optional().describe("Hiring manager or recruiter name"),
53
86
  contactTitle: z.string().optional().describe("Contact job title"),
54
87
  contactEmail: z.string().optional().describe("Contact email for cold outreach"),
55
- mode: z.enum(["standard", "cold_outreach"]).optional().describe("Generation mode"),
56
- model: z.enum(["flash", "pro"]).optional().describe("AI model: flash (faster/cheaper) or pro (higher quality)"),
57
- autoSelectContacts: z.boolean().optional().describe("Set false for phased execution with contact selection gate. Default true for API keys."),
88
+ genericEmail: z.string().optional().describe("Generic company email (e.g. careers@company.com) as fallback"),
89
+ emailAddresses: z.string().optional().describe("Comma-separated email addresses for outreach"),
58
90
  originalCV: z.string().optional().describe("Original CV text to use directly instead of master resume"),
59
91
  extensiveCV: z.string().optional().describe("Extended/detailed CV text for richer generation"),
60
- skipResearchCache: z.boolean().optional().describe("Force fresh company research instead of using cache"),
92
+ cvStrategy: z.string().optional().describe("Strategy instructions for CV generation"),
93
+ coverLetterStrategy: z.string().optional().describe("Strategy instructions for cover letter generation"),
94
+ coldEmailStrategy: z.string().optional().describe("Strategy instructions for cold email generation"),
95
+ reconStrategy: z.string().optional().describe("Strategy instructions for company research/recon phase"),
96
+ specificCore: z.string().optional().describe("Specific core competencies or skills to emphasize"),
97
+ companyProfile: z.string().optional().describe("Pre-researched company profile to skip research phase"),
61
98
  jobSourceUrl: z.string().optional().describe("URL of the original job posting")
62
99
  },
63
100
  async (params) => {
@@ -65,19 +102,31 @@ function registerAgentTools(server2, client2) {
65
102
  const result = await client2.agents.run("job-hunter", {
66
103
  companyName: params.companyName,
67
104
  jobTitle: params.jobTitle,
68
- ...params.jobDescription != null && { jobDescription: params.jobDescription },
69
- ...params.companyWebsite != null && { companyWebsite: params.companyWebsite },
105
+ jobDescription: params.jobDescription,
106
+ companyWebsite: params.companyWebsite,
107
+ ...params.sessionId != null && { sessionId: params.sessionId },
108
+ ...params.generationId != null && { generationId: params.generationId },
70
109
  ...params.masterResumeId != null && { masterResumeId: params.masterResumeId },
110
+ ...params.tier != null && { tier: params.tier },
111
+ ...params.model != null && { model: params.model },
112
+ ...params.webhookUrl != null && { webhookUrl: params.webhookUrl },
113
+ ...params.mode != null && { mode: params.mode },
71
114
  ...params.theme != null && { theme: params.theme },
115
+ ...params.autoSelectContacts != null && { autoSelectContacts: params.autoSelectContacts },
116
+ ...params.skipResearchCache != null && { skipResearchCache: params.skipResearchCache },
72
117
  ...params.contactName != null && { contactName: params.contactName },
73
118
  ...params.contactTitle != null && { contactTitle: params.contactTitle },
74
119
  ...params.contactEmail != null && { contactEmail: params.contactEmail },
75
- ...params.mode != null && { mode: params.mode },
76
- ...params.model != null && { model: params.model },
77
- ...params.autoSelectContacts != null && { autoSelectContacts: params.autoSelectContacts },
120
+ ...params.genericEmail != null && { genericEmail: params.genericEmail },
121
+ ...params.emailAddresses != null && { emailAddresses: params.emailAddresses },
78
122
  ...params.originalCV != null && { originalCV: params.originalCV },
79
123
  ...params.extensiveCV != null && { extensiveCV: params.extensiveCV },
80
- ...params.skipResearchCache != null && { skipResearchCache: params.skipResearchCache },
124
+ ...params.cvStrategy != null && { cvStrategy: params.cvStrategy },
125
+ ...params.coverLetterStrategy != null && { coverLetterStrategy: params.coverLetterStrategy },
126
+ ...params.coldEmailStrategy != null && { coldEmailStrategy: params.coldEmailStrategy },
127
+ ...params.reconStrategy != null && { reconStrategy: params.reconStrategy },
128
+ ...params.specificCore != null && { specificCore: params.specificCore },
129
+ ...params.companyProfile != null && { companyProfile: params.companyProfile },
81
130
  ...params.jobSourceUrl != null && { jobSourceUrl: params.jobSourceUrl }
82
131
  });
83
132
  return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
@@ -88,30 +137,54 @@ function registerAgentTools(server2, client2) {
88
137
  );
89
138
  server2.tool(
90
139
  "b2b-sales-run",
91
- "Run the B2B Sales agent \u2014 researches a company and generates personalized sales outreach. Consumes 50-500 credits. Rate limited: 10 req/min. Requires scope: sales:write. Check balance with settings-usage-summary before running.",
140
+ "Run the B2B Sales agent \u2014 researches a company and generates personalized sales outreach (emails, follow-ups). Supports phased execution: set autoSelectContacts=false to pause for contact selection, autoApproveDraft=false to review before sending. Poll with agent-status, respond with agent-interact. Consumes 50-500 credits. Rate limited: 10 req/min. Requires scope: sales:write. Check balance with settings-usage-summary before running.",
92
141
  {
93
142
  companyName: z.string().describe("Target company name"),
94
143
  companyWebsite: z.string().describe("Target company website URL"),
95
- targetProfile: z.record(z.unknown()).optional().describe("Target contact profile object (name, title, email, etc.)"),
96
- sessionId: z.string().optional().describe("Existing session ID to continue"),
97
- strategy: z.string().optional().describe("Sales strategy to use"),
98
- webhookUrl: z.string().optional().describe("Webhook URL for async status updates"),
144
+ sessionId: z.string().optional().describe("Existing session ID to continue a previous run"),
145
+ generationId: z.string().optional().describe("Existing generation ID to continue"),
99
146
  model: z.enum(["flash", "pro"]).optional().describe("AI model: flash (faster/cheaper) or pro (higher quality)"),
100
147
  userCompanyContext: z.string().optional().describe("Context about your own company for personalization"),
101
- autoSelectContacts: z.boolean().optional().describe("Set false for phased execution with contact selection gate")
148
+ targetCompanyContext: z.string().optional().describe("Pre-researched context about the target company"),
149
+ contactName: z.string().optional().describe("Target contact name"),
150
+ contactTitle: z.string().optional().describe("Target contact job title"),
151
+ contactEmail: z.string().optional().describe("Target contact email address"),
152
+ senderName: z.string().optional().describe("Name of the person sending the outreach"),
153
+ salesStrategy: z.string().optional().describe("Sales strategy to use for outreach generation"),
154
+ reconStrategy: z.string().optional().describe("Strategy instructions for company research/recon phase"),
155
+ companyResearch: z.string().optional().describe("Pre-researched company information to skip research phase"),
156
+ researchMode: z.enum(["parallel", "sequential"]).optional().describe("Research execution mode: parallel (faster) or sequential"),
157
+ autoSelectContacts: z.boolean().optional().describe("Set false for phased execution with contact selection gate"),
158
+ autoApproveDraft: z.boolean().optional().describe("Set false to pause for draft review before finalizing"),
159
+ autoApproveFollowups: z.boolean().optional().describe("Set false to pause for follow-up review before finalizing"),
160
+ followUpCount: z.number().optional().describe("Number of follow-up emails to generate"),
161
+ followUpDelayDays: z.number().optional().describe("Days between follow-up emails"),
162
+ skipResearchCache: z.boolean().optional().describe("Force fresh company research instead of using cache")
102
163
  },
103
164
  async (params) => {
104
165
  try {
105
166
  const result = await client2.agents.run("b2b-sales", {
106
167
  companyName: params.companyName,
107
168
  companyWebsite: params.companyWebsite,
108
- ...params.targetProfile != null && { targetProfile: params.targetProfile },
109
169
  ...params.sessionId != null && { sessionId: params.sessionId },
110
- ...params.strategy != null && { strategy: params.strategy },
111
- ...params.webhookUrl != null && { webhookUrl: params.webhookUrl },
170
+ ...params.generationId != null && { generationId: params.generationId },
112
171
  ...params.model != null && { model: params.model },
113
172
  ...params.userCompanyContext != null && { userCompanyContext: params.userCompanyContext },
114
- ...params.autoSelectContacts != null && { autoSelectContacts: params.autoSelectContacts }
173
+ ...params.targetCompanyContext != null && { targetCompanyContext: params.targetCompanyContext },
174
+ ...params.contactName != null && { contactName: params.contactName },
175
+ ...params.contactTitle != null && { contactTitle: params.contactTitle },
176
+ ...params.contactEmail != null && { contactEmail: params.contactEmail },
177
+ ...params.senderName != null && { senderName: params.senderName },
178
+ ...params.salesStrategy != null && { salesStrategy: params.salesStrategy },
179
+ ...params.reconStrategy != null && { reconStrategy: params.reconStrategy },
180
+ ...params.companyResearch != null && { companyResearch: params.companyResearch },
181
+ ...params.researchMode != null && { researchMode: params.researchMode },
182
+ ...params.autoSelectContacts != null && { autoSelectContacts: params.autoSelectContacts },
183
+ ...params.autoApproveDraft != null && { autoApproveDraft: params.autoApproveDraft },
184
+ ...params.autoApproveFollowups != null && { autoApproveFollowups: params.autoApproveFollowups },
185
+ ...params.followUpCount != null && { followUpCount: params.followUpCount },
186
+ ...params.followUpDelayDays != null && { followUpDelayDays: params.followUpDelayDays },
187
+ ...params.skipResearchCache != null && { skipResearchCache: params.skipResearchCache }
115
188
  });
116
189
  return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
117
190
  } catch (err) {
@@ -121,7 +194,7 @@ function registerAgentTools(server2, client2) {
121
194
  );
122
195
  server2.tool(
123
196
  "agent-status",
124
- "Check the status of a running agent job. Requires scope: jobs:read or sales:read.",
197
+ "Poll the status of a running agent job to check progress, completion, or if it is awaiting input. Returns status, logs, and artifacts when available. Use this after calling job-hunter-run or b2b-sales-run to monitor the asynchronous job. When status is awaiting_input, use agent-interact to respond. Read-only, no side effects. Requires scope: jobs:read or sales:read.",
125
198
  {
126
199
  agentType: z.enum(["job-hunter", "b2b-sales"]).describe("Agent type"),
127
200
  jobId: z.string().describe("Job ID returned from a generate call"),
@@ -140,7 +213,7 @@ function registerAgentTools(server2, client2) {
140
213
  );
141
214
  server2.tool(
142
215
  "agent-interact",
143
- "Submit a response to a phased agent workflow that is awaiting input. Used when agent-status returns awaiting_input. Rate limited: 10 req/min. Requires scope: jobs:write or sales:write.",
216
+ "Submit a response to a phased agent workflow that is awaiting input (e.g. contact selection, draft approval). Used when agent-status returns status=awaiting_input. The interactionType and interactionData fields come from the awaiting_input response. Rate limited: 10 req/min. Requires scope: jobs:write or sales:write.",
144
217
  {
145
218
  agentType: z.enum(["job-hunter", "b2b-sales"]).describe("Agent type"),
146
219
  generationId: z.string().describe("Generation ID from the run or status response"),
@@ -164,20 +237,14 @@ function registerAgentTools(server2, client2) {
164
237
  );
165
238
  server2.tool(
166
239
  "job-hunter-generate-cv",
167
- "Generate a CV synchronously without running the full Job Hunter pipeline. Faster but produces only a CV, no cover letter or cold email. Consumes credits. Rate limited: 10 req/min. Requires scope: jobs:write.",
240
+ "Generate a CV synchronously without running the full Job Hunter pipeline. Faster but produces only a CV, no cover letter or cold email. Pass a prompt describing the desired CV content, style, or modifications. Consumes credits. Rate limited: 10 req/min. Requires scope: jobs:write.",
168
241
  {
169
- prompt: z.string().describe("Prompt for CV generation \u2014 describe the desired CV content, style, or modifications"),
170
- resume: z.record(z.unknown()).optional().describe("Resume data object in JSON Resume format"),
171
- jobDescription: z.string().optional().describe("Job description text for tailoring"),
172
- theme: z.enum(["even", "stackoverflow", "class", "professional", "elegant", "macchiato", "react", "academic"]).optional().describe("Resume theme")
242
+ prompt: z.string().describe("Prompt for CV generation \u2014 describe the desired CV content, style, or modifications")
173
243
  },
174
244
  async (params) => {
175
245
  try {
176
246
  const result = await client2.agents.generateCv({
177
- prompt: params.prompt,
178
- ...params.resume != null && { resume: params.resume },
179
- ...params.jobDescription != null && { jobDescription: params.jobDescription },
180
- ...params.theme != null && { theme: params.theme }
247
+ prompt: params.prompt
181
248
  });
182
249
  return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
183
250
  } catch (err) {
@@ -187,7 +254,7 @@ function registerAgentTools(server2, client2) {
187
254
  );
188
255
  server2.tool(
189
256
  "agent-manifest",
190
- "Get the manifest (input fields, capabilities, billing) for an agent type. Requires scope: jobs:read or sales:read.",
257
+ "Get the manifest for an agent type, describing its input fields, capabilities, supported options, and billing information (credit costs per action). Use this to discover what parameters an agent accepts before running it, or to display pricing information. Read-only, no side effects. Requires scope: jobs:read or sales:read.",
191
258
  {
192
259
  agentType: z.enum(["job-hunter", "b2b-sales"]).describe("Agent type")
193
260
  },
@@ -207,10 +274,8 @@ import { z as z2 } from "zod";
207
274
  function registerAtsTools(server2, client2) {
208
275
  server2.tool(
209
276
  "ats-score",
210
- "Score a resume against a job description for ATS compatibility. Returns overall score, grade, keyword matches, and improvement suggestions. Consumes credits. Requires scope: ats:write.",
277
+ "Score a resume against a job description for ATS (Applicant Tracking System) compatibility using a 3-pass hybrid analysis (keyword extraction, deterministic matching, semantic gap analysis). Returns an overall score, letter grade, matched/missing keywords, and actionable improvement suggestions. Use this before job-hunter-run to assess resume fit, or standalone to evaluate how well a resume matches a specific job posting. Consumes credits. Requires scope: ats:write. Use upload-job-text to parse a job description first if you have a URL.",
211
278
  {
212
- // TODO(SDK): SDK type uses `resume: Record<string, unknown>` and `jobDescription: Record<string, unknown>`
213
- // but the actual API expects string fields. Remove cast once SDK types are fixed.
214
279
  resumeText: z2.string().describe("Resume as plain text"),
215
280
  jobDescription: z2.string().describe("Job description as plain text"),
216
281
  jobTitle: z2.string().optional().describe("Job title for additional context")
@@ -235,7 +300,7 @@ import { z as z3 } from "zod";
235
300
  function registerResumeTools(server2, client2) {
236
301
  server2.tool(
237
302
  "resume-parse",
238
- "Parse a resume file into structured JSON Resume format. Accepts base64-encoded file content. Requires scope: resume:write.",
303
+ "Parse a resume file (PDF, DOCX, or TXT) into structured JSON Resume format. Accepts base64-encoded file content and returns structured data with contact info, work experience, education, and skills. Use this to extract structured data from an existing resume file. For uploading and parsing in one step, use upload-resume instead. Requires scope: resume:write.",
239
304
  {
240
305
  fileBase64: z3.string().describe("Base64-encoded resume file (PDF, DOCX, TXT)"),
241
306
  filename: z3.string().describe("Original filename with extension"),
@@ -258,7 +323,7 @@ function registerResumeTools(server2, client2) {
258
323
  );
259
324
  server2.tool(
260
325
  "resume-validate",
261
- "Validate a resume in JSON Resume format. Returns validation errors and warnings. Requires scope: resume:write.",
326
+ "Validate a resume object against the JSON Resume schema, returning any errors and warnings (missing fields, invalid formats, incomplete sections). Use this after parsing or editing a resume to verify it is well-formed before rendering or submitting to agents. Does not modify the resume. Requires scope: resume:write.",
262
327
  {
263
328
  resume: z3.record(z3.unknown()).describe("Resume object in JSON Resume format")
264
329
  },
@@ -273,7 +338,7 @@ function registerResumeTools(server2, client2) {
273
338
  );
274
339
  server2.tool(
275
340
  "resume-render",
276
- "Render a resume to PDF or HTML. Returns a URL to download the generated file. Requires scope: resume:write.",
341
+ "Render a JSON Resume object to a downloadable PDF or HTML file using a specified theme. Returns a URL to download the generated file. Use this to produce a polished, formatted resume for sharing or printing. Use resume-themes to see available themes. For a quick inline preview without generating a file, use resume-preview instead. Requires scope: resume:write.",
277
342
  {
278
343
  resume: z3.record(z3.unknown()).describe("Resume object in JSON Resume format"),
279
344
  theme: z3.string().describe("Theme name (e.g. even, stackoverflow, class, professional, elegant, macchiato, react, academic)"),
@@ -294,7 +359,7 @@ function registerResumeTools(server2, client2) {
294
359
  );
295
360
  server2.tool(
296
361
  "resume-preview",
297
- "Preview a resume as HTML. Returns rendered HTML string. Requires scope: resume:write.",
362
+ "Preview a JSON Resume object as inline HTML using a specified theme. Returns the rendered HTML string directly (not a URL). Use this for quick visual previews without generating a downloadable file. For producing a downloadable PDF or HTML file, use resume-render instead. Requires scope: resume:write.",
298
363
  {
299
364
  resume: z3.record(z3.unknown()).describe("Resume object in JSON Resume format"),
300
365
  theme: z3.string().describe("Theme name (e.g. even, stackoverflow, class, professional)")
@@ -313,7 +378,7 @@ function registerResumeTools(server2, client2) {
313
378
  );
314
379
  server2.tool(
315
380
  "resume-themes",
316
- "List all available resume themes. Returns theme IDs, names, and descriptions. Requires scope: resume:read.",
381
+ "List all available resume themes with their IDs, names, and descriptions. Use this to discover theme options before calling resume-render or resume-preview, or to let the user choose a theme. Read-only, no side effects. Requires scope: resume:read.",
317
382
  {},
318
383
  async () => {
319
384
  try {
@@ -326,7 +391,7 @@ function registerResumeTools(server2, client2) {
326
391
  );
327
392
  server2.tool(
328
393
  "resume-import-rx",
329
- "Import a resume from Reactive Resume (RxResume) format into JSON Resume format. Requires scope: resume:write.",
394
+ "Convert a resume from Reactive Resume (RxResume) format into JSON Resume format. Use this to import resumes exported from the Reactive Resume application. Returns the converted resume object. Does not store the result; save it with master-resume-upsert if needed. Requires scope: resume:write. For the reverse conversion, use resume-export-rx.",
330
395
  {
331
396
  data: z3.record(z3.unknown()).describe("RxResume data object to import")
332
397
  },
@@ -341,7 +406,7 @@ function registerResumeTools(server2, client2) {
341
406
  );
342
407
  server2.tool(
343
408
  "resume-export-rx",
344
- "Export a JSON Resume to Reactive Resume (RxResume) format. Requires scope: resume:read.",
409
+ "Convert a JSON Resume object to Reactive Resume (RxResume) format for use in the Reactive Resume application. Optionally include design/styling configuration. Read-only conversion, does not modify the source resume. Requires scope: resume:read. For the reverse conversion, use resume-import-rx.",
345
410
  {
346
411
  resume: z3.record(z3.unknown()).describe("Resume object in JSON Resume format"),
347
412
  designBlob: z3.record(z3.unknown()).optional().describe("Optional design/styling configuration")
@@ -359,33 +424,12 @@ function registerResumeTools(server2, client2) {
359
424
  }
360
425
  );
361
426
  server2.tool(
362
- "master-resume-create",
363
- "Create a new master resume. Returns the saved master resume with its ID. Requires scope: resume:write.",
364
- {
365
- name: z3.string().describe("Name/label for this master resume"),
366
- resume: z3.record(z3.unknown()).describe("Resume data object (structured JSON Resume or raw text in a wrapper)"),
367
- metadata: z3.record(z3.unknown()).optional().describe("Optional metadata for the master resume")
368
- },
369
- async (params) => {
370
- try {
371
- const result = await client2.resume.createMaster({
372
- name: params.name,
373
- resume: params.resume,
374
- ...params.metadata != null && { metadata: params.metadata }
375
- });
376
- return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
377
- } catch (err) {
378
- return handleToolError(err);
379
- }
380
- }
381
- );
382
- server2.tool(
383
- "master-resume-list",
384
- "List all master resumes. Returns an array of master resume objects. Requires scope: resume:read.",
427
+ "master-resume-get",
428
+ "Get the user's stored master resume, returning its label, raw text, and structured data. The master resume serves as the default resume for agent runs when no other resume is provided. Use this to review the current master resume before running job-hunter-run. Read-only, no side effects. Requires scope: resume:read. Use master-resume-upsert to create or update it.",
385
429
  {},
386
430
  async () => {
387
431
  try {
388
- const result = await client2.resume.listMasters();
432
+ const result = await client2.resume.getMaster();
389
433
  return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
390
434
  } catch (err) {
391
435
  return handleToolError(err);
@@ -393,35 +437,19 @@ function registerResumeTools(server2, client2) {
393
437
  }
394
438
  );
395
439
  server2.tool(
396
- "master-resume-get",
397
- "Get a master resume by ID. Returns the full master resume object. Requires scope: resume:read.",
440
+ "master-resume-upsert",
441
+ "Create or replace the user's master resume (upsert). The master resume is used as the default resume for job-hunter-run when no other resume is provided. Overwrites any existing master resume. Requires scope: resume:write. Use master-resume-get to check if one already exists. Use resume-parse to extract structured data from a file before saving.",
398
442
  {
399
- id: z3.string().describe("Master resume ID")
443
+ label: z3.string().describe("Label for the master resume"),
444
+ rawText: z3.string().describe("Raw text content of the resume"),
445
+ structuredData: z3.record(z3.unknown()).optional().describe("Optional structured resume data")
400
446
  },
401
447
  async (params) => {
402
448
  try {
403
- const result = await client2.resume.getMaster(params.id);
404
- return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
405
- } catch (err) {
406
- return handleToolError(err);
407
- }
408
- }
409
- );
410
- server2.tool(
411
- "master-resume-update",
412
- "Update a master resume by ID. Returns the updated master resume. Requires scope: resume:write.",
413
- {
414
- id: z3.string().describe("Master resume ID"),
415
- name: z3.string().optional().describe("Updated name/label"),
416
- resume: z3.record(z3.unknown()).optional().describe("Updated resume data object"),
417
- metadata: z3.record(z3.unknown()).optional().describe("Updated metadata")
418
- },
419
- async (params) => {
420
- try {
421
- const result = await client2.resume.updateMaster(params.id, {
422
- ...params.name != null && { name: params.name },
423
- ...params.resume != null && { resume: params.resume },
424
- ...params.metadata != null && { metadata: params.metadata }
449
+ const result = await client2.resume.upsertMaster({
450
+ label: params.label,
451
+ rawText: params.rawText,
452
+ ...params.structuredData != null && { structuredData: params.structuredData }
425
453
  });
426
454
  return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
427
455
  } catch (err) {
@@ -431,14 +459,12 @@ function registerResumeTools(server2, client2) {
431
459
  );
432
460
  server2.tool(
433
461
  "master-resume-delete",
434
- "Delete a master resume by ID. Requires scope: resume:write.",
435
- {
436
- id: z3.string().describe("Master resume ID")
437
- },
438
- async (params) => {
462
+ "Permanently delete the user's master resume. After deletion, agent runs will require a resume to be provided directly. This is irreversible. Requires scope: resume:write. Use master-resume-get to review the resume before deleting.",
463
+ {},
464
+ async () => {
439
465
  try {
440
- await client2.resume.deleteMaster(params.id);
441
- return { content: [{ type: "text", text: JSON.stringify({ success: true, message: "Master resume deleted", id: params.id }) }] };
466
+ const result = await client2.resume.deleteMaster();
467
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
442
468
  } catch (err) {
443
469
  return handleToolError(err);
444
470
  }
@@ -451,7 +477,7 @@ import { z as z4 } from "zod";
451
477
  function registerUploadTools(server2, client2) {
452
478
  server2.tool(
453
479
  "upload-resume",
454
- "Upload and parse a resume file. Accepts base64-encoded file content. Returns parsed resume data. Requires scope: upload:write.",
480
+ "Upload a resume file (PDF, DOCX, TXT) as base64-encoded content and parse it into structured data. Returns extracted contact info, work experience, education, and skills. Use this as the first step in a job application workflow before running ats-score or job-hunter-run. Max file size ~10 MB. Requires scope: upload:write. For parsing without the upload step, use resume-parse instead.",
455
481
  {
456
482
  fileBase64: z4.string().max(13981014).describe("Base64-encoded file content (PDF, DOCX, etc.) \u2014 max ~10 MB"),
457
483
  filename: z4.string().describe("Original filename with extension (e.g. resume.pdf)"),
@@ -472,9 +498,9 @@ function registerUploadTools(server2, client2) {
472
498
  );
473
499
  server2.tool(
474
500
  "upload-job-file",
475
- "Upload and parse a job description file. Accepts base64-encoded file content. Returns parsed job data. Requires scope: upload:write.",
501
+ "Upload a job description file (PDF, DOCX, TXT) as base64-encoded content and parse it into structured job data. Returns extracted job title, requirements, qualifications, and company info. Use this when the job description is in a file rather than plain text. Max file size ~10 MB. Requires scope: upload:write. For plain text or URL-based job descriptions, use upload-job-text instead.",
476
502
  {
477
- fileBase64: z4.string().describe("Base64-encoded file content (PDF, DOCX, etc.)"),
503
+ fileBase64: z4.string().max(13981014).describe("Base64-encoded file content (PDF, DOCX, etc.) \u2014 max ~10 MB"),
478
504
  filename: z4.string().describe("Original filename with extension"),
479
505
  contentType: z4.string().optional().describe("MIME type")
480
506
  },
@@ -493,10 +519,10 @@ function registerUploadTools(server2, client2) {
493
519
  );
494
520
  server2.tool(
495
521
  "upload-job-text",
496
- "Upload a job description as plain text or fetch from a URL. At least one of text or url is required. Returns parsed job data. Requires scope: upload:write.",
522
+ "Parse a job description from plain text or fetch and parse from a URL. Returns structured job data including title, requirements, and qualifications. Use this when you have the job description as text or a URL to a job posting page. At least one of text or url is required. Requires scope: upload:write. For file-based job descriptions (PDF, DOCX), use upload-job-file instead.",
497
523
  {
498
524
  text: z4.string().max(5e4).optional().describe("Job description text (max 50K characters). Required if url is not provided."),
499
- url: z4.string().optional().describe("URL to fetch job description from. Required if text is not provided."),
525
+ url: z4.string().url().max(2048).optional().describe("URL to fetch job description from. Required if text is not provided."),
500
526
  source: z4.string().optional().describe("Source label/identifier for the job posting")
501
527
  },
502
528
  async (params) => {
@@ -525,15 +551,15 @@ import { z as z5 } from "zod";
525
551
  function registerSessionTools(server2, client2) {
526
552
  server2.tool(
527
553
  "session-create",
528
- "Create a new session. Returns the created session object with its ID. Requires scope: sessions:write.",
554
+ "Create a new session for grouping agent runs and their artifacts. Returns the session object with its ID. Sessions organize multiple generations (agent runs) into a logical workspace. Use this before running job-hunter-run or b2b-sales-run with a specific sessionId. Requires scope: sessions:write.",
529
555
  {
530
- agentType: z5.enum(["job-hunter", "b2b-sales"]).describe("Agent type for the session"),
556
+ sessionId: z5.string().optional().describe("Optional client-generated session ID"),
531
557
  metadata: z5.record(z5.unknown()).optional().describe("Optional session metadata")
532
558
  },
533
559
  async (params) => {
534
560
  try {
535
561
  const result = await client2.sessions.create({
536
- agentType: params.agentType,
562
+ ...params.sessionId != null && { sessionId: params.sessionId },
537
563
  ...params.metadata != null && { metadata: params.metadata }
538
564
  });
539
565
  return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
@@ -544,18 +570,16 @@ function registerSessionTools(server2, client2) {
544
570
  );
545
571
  server2.tool(
546
572
  "session-list",
547
- "List sessions with optional filtering and pagination. Returns an array of session objects. Requires scope: sessions:read. Note: API may use cursor-based pagination (cursor + limit) \u2014 page-based is the current SDK model.",
573
+ "List sessions with cursor-based pagination. Returns session objects with IDs, creation dates, and metadata. Use this to browse existing sessions or find a session to resume. When limit is provided, returns a paginated envelope with a cursor for the next page. Read-only, no side effects. Requires scope: sessions:read. Use session-get or session-hydrate to get full details for a specific session.",
548
574
  {
549
- page: z5.number().optional().describe("Page number for pagination"),
550
- limit: z5.number().optional().describe("Number of sessions per page"),
551
- agentType: z5.string().optional().describe("Filter by agent type (job-hunter or b2b-sales)")
575
+ cursor: z5.string().optional().describe("Cursor for pagination (ISO 8601 datetime string)"),
576
+ limit: z5.number().min(1).max(50).optional().describe("Number of sessions per page (1-50)")
552
577
  },
553
578
  async (params) => {
554
579
  try {
555
580
  const result = await client2.sessions.list({
556
- page: params.page,
557
- limit: params.limit,
558
- agentType: params.agentType
581
+ ...params.cursor != null && { cursor: params.cursor },
582
+ ...params.limit != null && { limit: params.limit }
559
583
  });
560
584
  return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
561
585
  } catch (err) {
@@ -565,7 +589,7 @@ function registerSessionTools(server2, client2) {
565
589
  );
566
590
  server2.tool(
567
591
  "session-get",
568
- "Get a session by ID. Returns the session object with generation history. Requires scope: sessions:read.",
592
+ "Get a session by ID, returning the session object with its generation history (list of agent runs). Use this to check what generations exist in a session without loading full artifacts. Read-only, no side effects. Requires scope: sessions:read. For full session data including all artifacts and logs, use session-hydrate instead.",
569
593
  {
570
594
  id: z5.string().describe("Session ID")
571
595
  },
@@ -580,7 +604,7 @@ function registerSessionTools(server2, client2) {
580
604
  );
581
605
  server2.tool(
582
606
  "session-hydrate",
583
- "Hydrate a session \u2014 returns the full session with all artifacts and logs. Requires scope: sessions:read.",
607
+ "Load a session with all its artifacts, logs, and generation details. Returns the complete session state including generated CVs, cover letters, emails, and conversation history. Use this to review full agent outputs or prepare artifacts for download. Heavier than session-get, so prefer session-get for lightweight lookups. Read-only, no side effects. Requires scope: sessions:read.",
584
608
  {
585
609
  id: z5.string().describe("Session ID")
586
610
  },
@@ -595,7 +619,7 @@ function registerSessionTools(server2, client2) {
595
619
  );
596
620
  server2.tool(
597
621
  "session-download",
598
- "Download an artifact from a session by its storage key. Returns base64-encoded content for binary files (PDF, DOCX, images) or plain text for text files. Requires scope: sessions:read.",
622
+ "Download a specific artifact from a session by its storage key. Returns base64-encoded content for binary files (PDF, DOCX, images) or plain text for text files. Use this to retrieve individual generated files like a rendered PDF resume or cover letter. Get artifact keys from session-hydrate first. Read-only, no side effects. Requires scope: sessions:read. For downloading by storage path instead of session context, use document-download.",
599
623
  {
600
624
  id: z5.string().describe("Session ID"),
601
625
  key: z5.string().describe("Artifact storage key from session hydration")
@@ -627,7 +651,7 @@ function registerSessionTools(server2, client2) {
627
651
  );
628
652
  server2.tool(
629
653
  "session-delete",
630
- "Delete a session by ID. Requires scope: sessions:write.",
654
+ "Permanently delete a session and all its generations, artifacts, and logs. This is irreversible. Use this to clean up completed or unwanted sessions. Requires scope: sessions:write. Use session-list to find sessions, and session-hydrate to review contents before deleting.",
631
655
  {
632
656
  id: z5.string().describe("Session ID")
633
657
  },
@@ -642,7 +666,7 @@ function registerSessionTools(server2, client2) {
642
666
  );
643
667
  server2.tool(
644
668
  "session-init",
645
- "Initialize session configuration \u2014 returns default settings and capabilities. Requires scope: sessions:read.",
669
+ "Get the default session configuration, including available agent types, supported features, and default settings. Use this to discover what agents and capabilities are available before creating sessions. Read-only, no side effects. Requires scope: sessions:read.",
646
670
  {},
647
671
  async () => {
648
672
  try {
@@ -655,7 +679,7 @@ function registerSessionTools(server2, client2) {
655
679
  );
656
680
  server2.tool(
657
681
  "session-log",
658
- "Append a log entry to a session. Used for tracking conversation history and tool usage. Requires scope: sessions:write.",
682
+ "Append a log entry to an existing session for tracking conversation history, tool usage, or status updates. Creates a new log record in the session. Use this to maintain an audit trail of actions taken during a session. Requires scope: sessions:write. Use session-hydrate to read back the full log history.",
659
683
  {
660
684
  id: z5.string().describe("Session ID"),
661
685
  role: z5.enum(["user", "assistant", "system", "tool", "status"]).describe("Log entry role"),
@@ -675,6 +699,23 @@ function registerSessionTools(server2, client2) {
675
699
  }
676
700
  }
677
701
  );
702
+ server2.tool(
703
+ "session-stats",
704
+ "Get aggregate session statistics for an agent type, including total sessions, active sequences, emails drafted, reply rate, and credits consumed. Use this to review overall usage and performance metrics for job-hunter or b2b-sales workflows. Read-only, no side effects. Requires scope: sessions:read.",
705
+ {
706
+ agentType: z5.enum(["job-hunter", "b2b-sales"]).describe("Agent type to get stats for")
707
+ },
708
+ async (params) => {
709
+ try {
710
+ const result = await client2.sessions.stats({
711
+ agentType: params.agentType
712
+ });
713
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
714
+ } catch (err) {
715
+ return handleToolError(err);
716
+ }
717
+ }
718
+ );
678
719
  }
679
720
 
680
721
  // src/tools/settings.ts
@@ -682,7 +723,7 @@ import { z as z6 } from "zod";
682
723
  function registerSettingsTools(server2, client2) {
683
724
  server2.tool(
684
725
  "settings-profile",
685
- "Get the current user's profile. Returns email, name, plan, and credit balance. Requires scope: settings:read.",
726
+ "Get the current user's profile including email, name, subscription plan, and remaining credit balance. Use this to check available credits before running agents (job-hunter-run, b2b-sales-run) or to verify account identity. Read-only, no side effects. Requires scope: settings:read. For detailed usage breakdown, use settings-usage-summary instead.",
686
727
  {},
687
728
  async () => {
688
729
  try {
@@ -695,13 +736,15 @@ function registerSettingsTools(server2, client2) {
695
736
  );
696
737
  server2.tool(
697
738
  "settings-preferences-get",
698
- "Get the current user's preferences. Requires scope: settings:read.",
739
+ "Get the current user's preferences, optionally filtered by agent type. Returns configuration like default themes, generation settings, and notification preferences. Read-only, no side effects. Requires scope: settings:read. Use settings-preferences-update to modify preferences.",
699
740
  {
700
- agentType: z6.string().optional().describe("Filter preferences by agent type (e.g. job-hunter, b2b-sales). Pending SDK support.")
741
+ agentType: z6.string().optional().describe("Filter preferences by agent type (e.g. job-hunter, b2b-sales)")
701
742
  },
702
743
  async (params) => {
703
744
  try {
704
- const result = await client2.settings.getPreferences();
745
+ const result = await client2.settings.getPreferences(
746
+ params.agentType != null ? { agentType: params.agentType } : void 0
747
+ );
705
748
  return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
706
749
  } catch (err) {
707
750
  return handleToolError(err);
@@ -710,16 +753,17 @@ function registerSettingsTools(server2, client2) {
710
753
  );
711
754
  server2.tool(
712
755
  "settings-preferences-update",
713
- "Update the current user's preferences. Returns the updated preferences. Requires scope: settings:write.",
756
+ "Update the current user's preferences, optionally scoped to a specific agent type. Modifies stored preferences and returns the updated values. Use this to configure default themes, generation settings, or notification preferences. Requires scope: settings:write. Use settings-preferences-get first to see current values before updating.",
714
757
  {
715
758
  preferences: z6.record(z6.unknown()).describe("Preferences object to update"),
716
- agentType: z6.string().optional().describe("Agent type to scope preferences to (e.g. job-hunter, b2b-sales). Pending SDK support.")
759
+ agentType: z6.string().optional().describe("Agent type to scope preferences to (e.g. job-hunter, b2b-sales)")
717
760
  },
718
761
  async (params) => {
719
762
  try {
720
- const result = await client2.settings.updatePreferences({
721
- preferences: params.preferences
722
- });
763
+ const result = await client2.settings.updatePreferences(
764
+ params.preferences,
765
+ params.agentType != null ? { agentType: params.agentType } : void 0
766
+ );
723
767
  return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
724
768
  } catch (err) {
725
769
  return handleToolError(err);
@@ -728,7 +772,7 @@ function registerSettingsTools(server2, client2) {
728
772
  );
729
773
  server2.tool(
730
774
  "settings-usage-summary",
731
- "Get a summary of the user's API usage and credit consumption. Requires scope: settings:read.",
775
+ "Get an aggregate summary of the user's API usage and credit consumption, including total credits used, remaining balance, and usage by category. Use this to check credit balance before running agents or to monitor spending. Read-only, no side effects. Requires scope: settings:read. For individual usage entries with timestamps, use settings-usage-logs instead.",
732
776
  {},
733
777
  async () => {
734
778
  try {
@@ -741,7 +785,7 @@ function registerSettingsTools(server2, client2) {
741
785
  );
742
786
  server2.tool(
743
787
  "settings-usage-logs",
744
- "Get paginated usage logs. Returns individual usage entries with action, credits, and timestamps. Requires scope: settings:read.",
788
+ "Get paginated usage logs showing individual entries with action type, credits consumed, and timestamps. Use this to audit specific credit charges or investigate unexpected usage. Read-only, no side effects. Requires scope: settings:read. For an aggregate overview, use settings-usage-summary instead.",
745
789
  {
746
790
  offset: z6.number().optional().describe("Offset for pagination"),
747
791
  limit: z6.number().max(100).optional().describe("Number of entries to return (max 100)")
@@ -760,9 +804,9 @@ function registerSettingsTools(server2, client2) {
760
804
  );
761
805
  server2.tool(
762
806
  "api-key-create",
763
- "Create a new platform API key. The key value is shown ONLY in this response -- save it immediately. Requires scope: settings:write.",
807
+ "Create a new platform API key with specified permission scopes. The key value is returned ONLY in this response and cannot be retrieved later, so save it immediately. Creates a new key record. Requires scope: settings:write. Use api-key-list to see existing keys. Use api-key-revoke to delete keys you no longer need.",
764
808
  {
765
- name: z6.string().describe("Human-readable name for the API key"),
809
+ label: z6.string().describe("Human-readable label for the API key"),
766
810
  scopes: z6.array(z6.enum([
767
811
  "*",
768
812
  "jobs:write",
@@ -778,7 +822,9 @@ function registerSettingsTools(server2, client2) {
778
822
  "resume:write",
779
823
  "ats:write",
780
824
  "webhook:read",
781
- "webhook:write"
825
+ "webhook:write",
826
+ "outreach:read",
827
+ "outreach:write"
782
828
  ])).describe("Permission scopes for the key. Use '*' for all scopes."),
783
829
  expiresAt: z6.string().optional().describe("Expiration date as ISO 8601 string"),
784
830
  monthlyCreditsLimit: z6.number().optional().describe("Monthly credit usage cap for this key")
@@ -786,7 +832,7 @@ function registerSettingsTools(server2, client2) {
786
832
  async (params) => {
787
833
  try {
788
834
  const result = await client2.settings.createApiKey({
789
- name: params.name,
835
+ label: params.label,
790
836
  scopes: params.scopes,
791
837
  ...params.expiresAt != null && { expiresAt: params.expiresAt },
792
838
  ...params.monthlyCreditsLimit != null && { monthlyCreditsLimit: params.monthlyCreditsLimit }
@@ -799,7 +845,7 @@ function registerSettingsTools(server2, client2) {
799
845
  );
800
846
  server2.tool(
801
847
  "api-key-list",
802
- "List all platform API keys. Returns key metadata (hash, name, scopes) -- NOT the key values. Requires scope: settings:read.",
848
+ "List all platform API keys for the current user. Returns key metadata (hash, label, scopes, creation date) but NOT the actual key values (those are only shown at creation time). Use this to find key hashes for revocation, rotation, or usage queries. Read-only, no side effects. Requires scope: settings:read.",
803
849
  {},
804
850
  async () => {
805
851
  try {
@@ -812,7 +858,7 @@ function registerSettingsTools(server2, client2) {
812
858
  );
813
859
  server2.tool(
814
860
  "api-key-revoke",
815
- "Revoke (delete) a platform API key by its hash. Requires scope: settings:write.",
861
+ "Permanently revoke and delete a platform API key by its hash. The key immediately stops working for all API calls. This is irreversible. Use this to remove compromised or unused keys. Requires scope: settings:write. Use api-key-list first to find the key hash. For replacing a key with a new one, use api-key-rotate instead.",
816
862
  {
817
863
  hash: z6.string().describe("API key hash to revoke")
818
864
  },
@@ -827,14 +873,17 @@ function registerSettingsTools(server2, client2) {
827
873
  );
828
874
  server2.tool(
829
875
  "api-key-rotate",
830
- "Rotate a platform API key -- revokes the old key and returns a new one. Save the new key immediately. Requires scope: settings:write.",
876
+ "Rotate a platform API key by revoking the old key and issuing a new one. The new key value is returned ONLY in this response, so save it immediately. An optional grace period keeps the old key valid during transition. Use this for periodic key rotation or when a key may be compromised but you need continuity. Requires scope: settings:write. For immediate revocation without replacement, use api-key-revoke instead.",
831
877
  {
832
878
  hash: z6.string().describe("API key hash to rotate"),
833
- gracePeriodHours: z6.number().optional().describe("Hours the old key remains valid after rotation (default: 24). Pending SDK support.")
879
+ gracePeriodHours: z6.number().optional().describe("Hours the old key remains valid after rotation (default: 24)")
834
880
  },
835
881
  async (params) => {
836
882
  try {
837
- const result = await client2.settings.rotateApiKey(params.hash);
883
+ const result = await client2.settings.rotateApiKey(
884
+ params.hash,
885
+ params.gracePeriodHours != null ? { gracePeriodHours: params.gracePeriodHours } : void 0
886
+ );
838
887
  return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
839
888
  } catch (err) {
840
889
  return handleToolError(err);
@@ -843,16 +892,13 @@ function registerSettingsTools(server2, client2) {
843
892
  );
844
893
  server2.tool(
845
894
  "api-key-usage",
846
- "Get usage statistics for a specific API key by its hash. Requires scope: settings:read. Note: uses direct HTTP (SDK method pending).",
895
+ "Get usage statistics for a specific API key by its hash, including request counts and credit consumption. Use this to monitor per-key usage or identify which key is consuming the most credits. Read-only, no side effects. Requires scope: settings:read. Use api-key-list to find key hashes.",
847
896
  {
848
897
  hash: z6.string().describe("API key hash")
849
898
  },
850
899
  async (params) => {
851
900
  try {
852
- const httpClient = client2.settings.httpClient;
853
- const result = await httpClient.request(
854
- `/settings/platform-api-keys/${encodeURIComponent(params.hash)}/usage`
855
- );
901
+ const result = await client2.settings.getApiKeyUsage(params.hash);
856
902
  return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
857
903
  } catch (err) {
858
904
  return handleToolError(err);
@@ -861,11 +907,11 @@ function registerSettingsTools(server2, client2) {
861
907
  );
862
908
  server2.tool(
863
909
  "byo-key-get",
864
- "Check if a Bring Your Own API key is configured and its status. Requires scope: settings:read.",
910
+ "Check the configuration status of all Bring Your Own (BYO) API keys. Returns each provider name and whether a key is configured. Use this to verify BYO key setup before running agents with tier=byo. Read-only, no side effects. Requires scope: settings:read. Use settings-supported-providers to see which providers are available. Use byo-key-set to configure a key.",
865
911
  {},
866
912
  async () => {
867
913
  try {
868
- const result = await client2.settings.getByoKey();
914
+ const result = await client2.settings.getProviderKeyStatus();
869
915
  return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
870
916
  } catch (err) {
871
917
  return handleToolError(err);
@@ -874,16 +920,17 @@ function registerSettingsTools(server2, client2) {
874
920
  );
875
921
  server2.tool(
876
922
  "byo-key-set",
877
- "Set a Bring Your Own API key for a provider (e.g. Gemini). BYO tier users get unlimited AI generation but still pay for contact enrichment. Requires scope: settings:write.",
923
+ "Configure a Bring Your Own (BYO) API key for an AI provider (e.g. gemini). BYO tier users get unlimited AI generation but still pay for contact enrichment credits. Stores the key securely on the platform. Requires scope: settings:write. Use settings-supported-providers to see available providers. Use byo-key-get to check current configuration. Use byo-key-remove to delete a configured key.",
878
924
  {
925
+ provider: z6.string().describe("Provider name (e.g. gemini)"),
879
926
  apiKey: z6.string().describe("The API key to set"),
880
- provider: z6.string().describe("Provider name (e.g. gemini)")
927
+ baseUrl: z6.string().optional().describe("Optional custom base URL for the provider")
881
928
  },
882
929
  async (params) => {
883
930
  try {
884
- const result = await client2.settings.setByoKey({
931
+ const result = await client2.settings.setProviderKey(params.provider, {
885
932
  apiKey: params.apiKey,
886
- provider: params.provider
933
+ ...params.baseUrl != null && { baseUrl: params.baseUrl }
887
934
  });
888
935
  return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
889
936
  } catch (err) {
@@ -893,12 +940,27 @@ function registerSettingsTools(server2, client2) {
893
940
  );
894
941
  server2.tool(
895
942
  "byo-key-remove",
896
- "Remove the configured Bring Your Own API key. Requires scope: settings:write.",
943
+ "Remove a configured Bring Your Own (BYO) API key for a provider. After removal, agent runs will use platform credits instead of the BYO key. Requires scope: settings:write. Use byo-key-get to check which providers have keys configured before removing.",
944
+ {
945
+ provider: z6.string().describe("Provider name to remove the key for (e.g. gemini)")
946
+ },
947
+ async (params) => {
948
+ try {
949
+ const result = await client2.settings.removeProviderKey(params.provider);
950
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
951
+ } catch (err) {
952
+ return handleToolError(err);
953
+ }
954
+ }
955
+ );
956
+ server2.tool(
957
+ "settings-supported-providers",
958
+ "List all AI providers that support Bring Your Own (BYO) API keys, including provider names and availability status. Use this to discover which providers can be configured with byo-key-set before setting up BYO keys. Read-only, no side effects. Requires scope: settings:read.",
897
959
  {},
898
960
  async () => {
899
961
  try {
900
- await client2.settings.removeByoKey();
901
- return { content: [{ type: "text", text: JSON.stringify({ success: true, message: "BYO key removed" }) }] };
962
+ const result = await client2.settings.getSupportedProviders();
963
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
902
964
  } catch (err) {
903
965
  return handleToolError(err);
904
966
  }
@@ -906,7 +968,7 @@ function registerSettingsTools(server2, client2) {
906
968
  );
907
969
  server2.tool(
908
970
  "webhook-secret-get",
909
- "Get the current webhook secret for verifying webhook signatures. Requires scope: webhook:read.",
971
+ "Get the current webhook signing secret used to verify webhook payloads from the LLM Conveyors platform. Use this when setting up webhook receivers to validate that incoming webhooks are authentic. Read-only, no side effects. Requires scope: webhook:read. Use webhook-secret-rotate to generate a new secret if the current one is compromised.",
910
972
  {},
911
973
  async () => {
912
974
  try {
@@ -919,7 +981,7 @@ function registerSettingsTools(server2, client2) {
919
981
  );
920
982
  server2.tool(
921
983
  "webhook-secret-rotate",
922
- "Rotate the webhook secret. Returns the new secret. The old secret is immediately invalidated. Rate limited: 5 per hour. Requires scope: webhook:write.",
984
+ "Rotate the webhook signing secret, generating a new one and immediately invalidating the old secret. All existing webhook receivers must be updated with the new secret or they will reject incoming payloads. Rate limited: 5 per hour. Requires scope: webhook:write. Use webhook-secret-get to retrieve the current secret before rotating.",
923
985
  {},
924
986
  async () => {
925
987
  try {
@@ -937,7 +999,7 @@ import { z as z7 } from "zod";
937
999
  function registerContentTools(server2, client2) {
938
1000
  server2.tool(
939
1001
  "content-save",
940
- "Save a source document for use as context in AI generation. Returns success status. Requires scope: settings:write.",
1002
+ "Save a source document that will be used as context in future AI generation runs (job-hunter-run, b2b-sales-run). Overwrites any existing document of the same type. Use this to store your CV, cover letter templates, or strategy documents before running agents. Requires scope: sessions:write. Use content-list-sources to see what is already saved. Use content-get-source to read a specific saved document.",
941
1003
  {
942
1004
  docType: z7.enum([
943
1005
  "original_cv",
@@ -965,18 +1027,14 @@ function registerContentTools(server2, client2) {
965
1027
  );
966
1028
  server2.tool(
967
1029
  "content-delete-generation",
968
- "Delete a generation and all its artifacts from a session. Requires scope: sessions:write. Note: uses direct HTTP because SDK lacks sessionId param.",
1030
+ "Permanently delete a generation and all its artifacts (CV, cover letter, emails) from a session. This is irreversible and removes all files associated with that generation. Use this to clean up unwanted outputs. Requires scope: sessions:write. Use session-hydrate first to review the generation before deleting.",
969
1031
  {
970
1032
  id: z7.string().describe("Generation ID to delete"),
971
1033
  sessionId: z7.string().describe("Session ID that owns the generation")
972
1034
  },
973
1035
  async (params) => {
974
1036
  try {
975
- const httpClient = client2.content.httpClient;
976
- const result = await httpClient.request(
977
- `/content/generations/${encodeURIComponent(params.id)}`,
978
- { method: "DELETE", query: { sessionId: params.sessionId } }
979
- );
1037
+ const result = await client2.content.deleteGeneration(params.id, params.sessionId);
980
1038
  return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
981
1039
  } catch (err) {
982
1040
  return handleToolError(err);
@@ -985,21 +1043,23 @@ function registerContentTools(server2, client2) {
985
1043
  );
986
1044
  server2.tool(
987
1045
  "content-research-sender",
988
- "Research and create a sender profile for content generation. Returns sender context. Requires scope: jobs:write or sales:write.",
1046
+ "Research a company and create a sender profile for personalized content generation. Returns structured sender context (company info, positioning, value propositions). Use this before b2b-sales-run to pre-populate sender context for better outreach personalization. Consumes credits. At least one of companyWebsite or companyName must be provided. Requires scope: sessions:write.",
989
1047
  {
990
1048
  companyWebsite: z7.string().optional().describe("Company website to research"),
991
1049
  companyName: z7.string().optional().describe("Company name for context")
992
1050
  },
993
1051
  async (params) => {
994
1052
  try {
995
- const httpClient = client2.content.httpClient;
996
- const result = await httpClient.request("/content/research-sender", {
997
- method: "POST",
998
- body: {
999
- ...params.companyWebsite != null && { companyWebsite: params.companyWebsite },
1000
- ...params.companyName != null && { companyName: params.companyName }
1001
- }
1002
- });
1053
+ if (!params.companyWebsite && !params.companyName) {
1054
+ return {
1055
+ content: [{ type: "text", text: JSON.stringify({ error: "At least one of companyWebsite or companyName is required" }, null, 2) }],
1056
+ isError: true
1057
+ };
1058
+ }
1059
+ const body = {};
1060
+ if (params.companyWebsite != null) body.companyWebsite = params.companyWebsite;
1061
+ if (params.companyName != null) body.companyName = params.companyName;
1062
+ const result = await client2.content.researchSender(body);
1003
1063
  return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
1004
1064
  } catch (err) {
1005
1065
  return handleToolError(err);
@@ -1008,12 +1068,11 @@ function registerContentTools(server2, client2) {
1008
1068
  );
1009
1069
  server2.tool(
1010
1070
  "content-list-sources",
1011
- "List all saved source documents used as context for AI generation. Requires scope: settings:read.",
1071
+ "List all saved source documents used as context for AI generation, showing document types and metadata. Use this to check what context documents are available before running agents. Read-only, no side effects. Requires scope: sessions:read. Use content-get-source to read a specific document. Use content-save to add or update documents.",
1012
1072
  {},
1013
1073
  async () => {
1014
1074
  try {
1015
- const httpClient = client2.content.httpClient;
1016
- const result = await httpClient.request("/content/sources");
1075
+ const result = await client2.content.listSources();
1017
1076
  return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
1018
1077
  } catch (err) {
1019
1078
  return handleToolError(err);
@@ -1022,7 +1081,7 @@ function registerContentTools(server2, client2) {
1022
1081
  );
1023
1082
  server2.tool(
1024
1083
  "content-get-source",
1025
- "Get a specific source document by type. Requires scope: settings:read.",
1084
+ "Retrieve the full content of a specific saved source document by type (e.g. original_cv, cv_strategy, cold_email_strategy). Use this to review or display a previously saved context document. Read-only, no side effects. Requires scope: sessions:read. Use content-list-sources to see all available document types.",
1026
1085
  {
1027
1086
  docType: z7.enum([
1028
1087
  "original_cv",
@@ -1037,8 +1096,7 @@ function registerContentTools(server2, client2) {
1037
1096
  },
1038
1097
  async (params) => {
1039
1098
  try {
1040
- const httpClient = client2.content.httpClient;
1041
- const result = await httpClient.request(`/content/sources/${encodeURIComponent(params.docType)}`);
1099
+ const result = await client2.content.getSource(params.docType);
1042
1100
  return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
1043
1101
  } catch (err) {
1044
1102
  return handleToolError(err);
@@ -1047,7 +1105,7 @@ function registerContentTools(server2, client2) {
1047
1105
  );
1048
1106
  server2.tool(
1049
1107
  "content-delete-source",
1050
- "Delete a source document by type. Requires scope: settings:write.",
1108
+ "Permanently delete a saved source document by type. Future agent runs will no longer use this document as context. Use this to remove outdated context documents before saving new ones. Requires scope: sessions:write. Use content-list-sources to verify which documents exist before deleting.",
1051
1109
  {
1052
1110
  docType: z7.enum([
1053
1111
  "original_cv",
@@ -1062,11 +1120,8 @@ function registerContentTools(server2, client2) {
1062
1120
  },
1063
1121
  async (params) => {
1064
1122
  try {
1065
- const httpClient = client2.content.httpClient;
1066
- const result = await httpClient.request(`/content/sources/${encodeURIComponent(params.docType)}`, {
1067
- method: "DELETE"
1068
- });
1069
- return { content: [{ type: "text", text: JSON.stringify(result ?? { success: true, message: "Source deleted", docType: params.docType }, null, 2) }] };
1123
+ const result = await client2.content.deleteSource(params.docType);
1124
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
1070
1125
  } catch (err) {
1071
1126
  return handleToolError(err);
1072
1127
  }
@@ -1079,7 +1134,7 @@ import { z as z8 } from "zod";
1079
1134
  function registerSharesTools(server2, client2) {
1080
1135
  server2.tool(
1081
1136
  "share-create",
1082
- "Create a shareable public link for a generation's artifacts. Returns a slug and public URL. Requires scope: sessions:write.",
1137
+ "Create a shareable public link for a generation's artifacts (resume, cover letter, emails). Returns a slug and public URL that anyone can view without authentication. Creates a new share record. Requires scope: sessions:read. Use after running job-hunter-run or b2b-sales-run to share the output. Use share-slug-stats to track views.",
1083
1138
  {
1084
1139
  sessionId: z8.string().describe("Session ID containing the generation"),
1085
1140
  generationId: z8.string().describe("Generation ID to share")
@@ -1098,7 +1153,7 @@ function registerSharesTools(server2, client2) {
1098
1153
  );
1099
1154
  server2.tool(
1100
1155
  "share-stats",
1101
- "Get statistics about your shared links (view counts, etc.). Requires scope: settings:read.",
1156
+ "Get aggregate statistics about all shared links for the current user, including total shares and total views. Use this for an overview of sharing activity. Read-only, no side effects. Requires scope: sessions:read. For per-link stats, use share-slug-stats with a specific slug.",
1102
1157
  {},
1103
1158
  async () => {
1104
1159
  try {
@@ -1111,7 +1166,7 @@ function registerSharesTools(server2, client2) {
1111
1166
  );
1112
1167
  server2.tool(
1113
1168
  "share-get-public",
1114
- "Get a publicly shared resource by its slug. No auth required (public endpoint).",
1169
+ "Retrieve a publicly shared resource by its slug. Returns the shared artifacts (resume, cover letter, emails) without requiring authentication. Use this to view what a share link contains. Read-only, no side effects. No auth required (public endpoint).",
1115
1170
  {
1116
1171
  slug: z8.string().describe("Public share slug")
1117
1172
  },
@@ -1126,16 +1181,13 @@ function registerSharesTools(server2, client2) {
1126
1181
  );
1127
1182
  server2.tool(
1128
1183
  "share-slug-stats",
1129
- "Get visit statistics for a specific share link (owner only). Requires scope: settings:read. Note: uses direct HTTP (SDK method pending).",
1184
+ "Get visit statistics for a specific share link, including view count and visit timestamps. Only accessible by the share owner. Use this to track engagement on a specific shared artifact. Read-only, no side effects. Requires scope: sessions:read. For aggregate stats across all shares, use share-stats instead.",
1130
1185
  {
1131
1186
  slug: z8.string().describe("Share link slug")
1132
1187
  },
1133
1188
  async (params) => {
1134
1189
  try {
1135
- const httpClient = client2.shares.httpClient;
1136
- const result = await httpClient.request(
1137
- `/shares/${encodeURIComponent(params.slug)}/stats`
1138
- );
1190
+ const result = await client2.shares.getShareStats(params.slug);
1139
1191
  return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
1140
1192
  } catch (err) {
1141
1193
  return handleToolError(err);
@@ -1149,7 +1201,7 @@ import { z as z9 } from "zod";
1149
1201
  function registerDocumentTools(server2, client2) {
1150
1202
  server2.tool(
1151
1203
  "document-download",
1152
- "Download a document by its storage path. Returns base64-encoded content for binary files (PDF, DOCX, images) or plain text for text files. Requires scope: sessions:read.",
1204
+ "Download a document artifact by its storage path. Returns base64-encoded content for binary files (PDF, DOCX, images) or plain text for text files. Use this to retrieve generated artifacts like rendered resumes or reports when you have the storage path. For downloading artifacts from a specific session, use session-download instead (which takes a session ID and artifact key). Read-only, no side effects. Requires scope: sessions:read.",
1153
1205
  {
1154
1206
  path: z9.string().describe("Document storage path")
1155
1207
  },
@@ -1185,7 +1237,7 @@ import { z as z10 } from "zod";
1185
1237
  function registerReferralTools(server2, client2) {
1186
1238
  server2.tool(
1187
1239
  "referral-stats",
1188
- "Get referral program statistics including total referrals and credits earned. Requires scope: settings:read.",
1240
+ "Get referral program statistics for the current user, including total referrals, successful conversions, and credits earned. Use this to check referral performance or display earnings. Read-only, no side effects. Requires scope: settings:read. Use referral-code to get the shareable referral link.",
1189
1241
  {},
1190
1242
  async () => {
1191
1243
  try {
@@ -1198,7 +1250,7 @@ function registerReferralTools(server2, client2) {
1198
1250
  );
1199
1251
  server2.tool(
1200
1252
  "referral-code",
1201
- "Get your referral code for sharing with others. Requires scope: settings:read.",
1253
+ "Get the current user's referral code and shareable referral link. Use this to retrieve the code for sharing with others. Read-only, no side effects. Requires scope: settings:read. To customize the code, use referral-vanity-code. To check referral performance, use referral-stats.",
1202
1254
  {},
1203
1255
  async () => {
1204
1256
  try {
@@ -1211,7 +1263,7 @@ function registerReferralTools(server2, client2) {
1211
1263
  );
1212
1264
  server2.tool(
1213
1265
  "referral-vanity-code",
1214
- "Set a custom vanity code for your referral link. Rate limited: 5 per hour. Requires scope: settings:write.",
1266
+ "Set a custom vanity code for the user's referral link, replacing the auto-generated code. This permanently changes the referral URL. Rate limited: 5 per hour. Requires scope: settings:write. Use referral-code first to see the current code before changing it.",
1215
1267
  {
1216
1268
  code: z10.string().describe("Custom vanity code to set")
1217
1269
  },
@@ -1239,7 +1291,7 @@ var CONSENT_PURPOSES = [
1239
1291
  function registerPrivacyTools(server2, client2) {
1240
1292
  server2.tool(
1241
1293
  "privacy-list-consents",
1242
- "List all privacy consent statuses for the current user. Returns consent purposes and their granted/revoked status. Requires scope: settings:read.",
1294
+ "List all privacy consent records for the current user, showing each data processing purpose and whether consent is granted or revoked. Use this to check consent status before running tools that require specific consents (e.g. ai-generation, contact-enrichment). Read-only, no side effects. Requires scope: settings:read.",
1243
1295
  {},
1244
1296
  async () => {
1245
1297
  try {
@@ -1252,7 +1304,7 @@ function registerPrivacyTools(server2, client2) {
1252
1304
  );
1253
1305
  server2.tool(
1254
1306
  "privacy-grant-consent",
1255
- "Grant consent for a specific data processing purpose. Requires scope: settings:write.",
1307
+ "Grant consent for a specific data processing purpose. This enables the platform to process data for that purpose (e.g. ai-generation enables agent runs, contact-enrichment enables contact lookups). Modifies the user's consent record. Requires scope: settings:write. Use privacy-list-consents to check current status first. Use privacy-revoke-consent to undo.",
1256
1308
  {
1257
1309
  purpose: z11.enum(CONSENT_PURPOSES).describe("Consent purpose to grant")
1258
1310
  },
@@ -1267,7 +1319,7 @@ function registerPrivacyTools(server2, client2) {
1267
1319
  );
1268
1320
  server2.tool(
1269
1321
  "privacy-revoke-consent",
1270
- "Revoke consent for a specific data processing purpose. Requires scope: settings:write.",
1322
+ "Revoke a previously granted consent for a specific data processing purpose. This may disable platform features that depend on that consent (e.g. revoking ai-generation prevents agent runs). Modifies the user's consent record. Requires scope: settings:write. Use privacy-list-consents to check current status first.",
1271
1323
  {
1272
1324
  purpose: z11.enum(CONSENT_PURPOSES).describe("Consent purpose to revoke")
1273
1325
  },
@@ -1286,7 +1338,7 @@ function registerPrivacyTools(server2, client2) {
1286
1338
  function registerHealthTools(server2, client2) {
1287
1339
  server2.tool(
1288
1340
  "health-root",
1289
- "Get API root info (name, version). Public endpoint, no auth required.",
1341
+ "Get the LLM Conveyors API server name and version. Use this to verify the MCP server is connected and reachable before running other tools. No authentication required. Returns JSON with name and version fields. For deeper diagnostics, use health-check instead.",
1290
1342
  {},
1291
1343
  async () => {
1292
1344
  try {
@@ -1299,7 +1351,7 @@ function registerHealthTools(server2, client2) {
1299
1351
  );
1300
1352
  server2.tool(
1301
1353
  "health-check",
1302
- "Check API health status \u2014 returns status, timestamp, uptime, version, checks, memory. Public endpoint, no auth required.",
1354
+ "Run a detailed health check on the LLM Conveyors API, returning status, uptime, version, dependency checks, and memory usage. Use this to diagnose connectivity or performance issues before retrying failed tool calls. No authentication required. For a simple alive/dead check, use health-live instead.",
1303
1355
  {},
1304
1356
  async () => {
1305
1357
  try {
@@ -1312,7 +1364,7 @@ function registerHealthTools(server2, client2) {
1312
1364
  );
1313
1365
  server2.tool(
1314
1366
  "health-ready",
1315
- "Check API readiness. Public endpoint, no auth required.",
1367
+ "Check whether the LLM Conveyors API is ready to accept requests (all dependencies initialized). Use this in automation pipelines to wait for readiness before sending agent runs. No authentication required. Returns a simple ready/not-ready status. For process liveness only, use health-live instead.",
1316
1368
  {},
1317
1369
  async () => {
1318
1370
  try {
@@ -1325,7 +1377,7 @@ function registerHealthTools(server2, client2) {
1325
1377
  );
1326
1378
  server2.tool(
1327
1379
  "health-live",
1328
- "Check API liveness. Public endpoint, no auth required.",
1380
+ "Check whether the LLM Conveyors API process is alive and responding. Use this as a lightweight heartbeat check in monitoring or retry loops. No authentication required. Returns a simple alive status. For dependency-level diagnostics, use health-check instead.",
1329
1381
  {},
1330
1382
  async () => {
1331
1383
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "llmconveyors-mcp",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
4
4
  "mcpName": "io.github.ebenezer-isaac/llmconveyors",
5
5
  "description": "MCP server that connects AI agents to LLM Conveyors — run Job Hunter, B2B Sales, and other AI agents from Claude, Cursor, or any MCP client",
6
6
  "type": "module",
@@ -51,7 +51,7 @@
51
51
  },
52
52
  "dependencies": {
53
53
  "@modelcontextprotocol/sdk": "^1.12.1",
54
- "llmconveyors": "^0.1.0",
54
+ "llmconveyors": "^0.3.0",
55
55
  "zod": "^3.24.0"
56
56
  },
57
57
  "devDependencies": {