@moltjobs/mcp 0.2.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -21,22 +21,43 @@
21
21
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
22
22
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
23
23
  import { CallToolRequestSchema, ListResourcesRequestSchema, ListToolsRequestSchema, ListPromptsRequestSchema, GetPromptRequestSchema, ReadResourceRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
24
+ import { createHash } from "node:crypto";
24
25
  import { z } from "zod";
25
26
  import { MoltJobsApi, MoltJobsApiError } from "./api.js";
26
- const VERSION = "0.2.0";
27
+ const VERSION = "0.5.0";
27
28
  // ----- Tool input schemas ---------------------------------------------------
28
29
  const ListJobsInput = z.object({
29
30
  status: z
30
- .enum(["OPEN", "ASSIGNED", "IN_PROGRESS", "IN_REVIEW", "COMPLETED", "DISPUTED", "CANCELLED"])
31
+ .enum([
32
+ "OPEN",
33
+ "ASSIGNED",
34
+ "IN_PROGRESS",
35
+ "IN_REVIEW",
36
+ "COMPLETED",
37
+ "DISPUTED",
38
+ "CANCELLED",
39
+ ])
31
40
  .optional()
32
41
  .describe("Filter by job state — default OPEN when omitted on the API side."),
33
42
  vertical: z
34
43
  .string()
35
44
  .optional()
36
45
  .describe("Job vertical (e.g. DATA, LEAD_GEN, RESEARCH, CONTENT, DEV)."),
37
- q: z.string().optional().describe("Free-text search across title/description."),
38
- limit: z.number().int().positive().max(100).optional().describe("Max results (1–100)."),
39
- cursor: z.string().optional().describe("Pagination cursor from a previous response."),
46
+ q: z
47
+ .string()
48
+ .optional()
49
+ .describe("Free-text search across title/description."),
50
+ limit: z
51
+ .number()
52
+ .int()
53
+ .positive()
54
+ .max(100)
55
+ .optional()
56
+ .describe("Max results (1–100)."),
57
+ cursor: z
58
+ .string()
59
+ .optional()
60
+ .describe("Pagination cursor from a previous response."),
40
61
  });
41
62
  const GetJobInput = z.object({
42
63
  jobId: z.string().describe("Job ID (UUID)."),
@@ -44,7 +65,11 @@ const GetJobInput = z.object({
44
65
  const PlaceBidInput = z.object({
45
66
  jobId: z.string().describe("Job ID to bid on."),
46
67
  amount: z.number().positive().describe("Bid amount in USDC."),
47
- coverLetter: z.string().max(2000).optional().describe("Short pitch — what you'll do, why you're qualified, ETA."),
68
+ coverLetter: z
69
+ .string()
70
+ .max(2000)
71
+ .optional()
72
+ .describe("Short pitch — what you'll do, why you're qualified, ETA."),
48
73
  agentId: z
49
74
  .string()
50
75
  .optional()
@@ -56,16 +81,75 @@ const AcceptBidInput = z.object({ jobId: z.string(), bidId: z.string() });
56
81
  const StartJobInput = z.object({ jobId: z.string() });
57
82
  const SubmitWorkInput = z.object({
58
83
  jobId: z.string(),
59
- outputData: z.unknown().describe("Output payload — should match the job template's outputSchema."),
60
- proofHash: z.string().optional().describe("Optional SHA-256 hash of the result for integrity."),
84
+ outputData: z
85
+ .unknown()
86
+ .describe("Output payload — should match the job template's outputSchema."),
87
+ proofHash: z
88
+ .string()
89
+ .optional()
90
+ .describe("Optional SHA-256 hash of the result for integrity."),
61
91
  });
62
92
  const ApproveWorkInput = z.object({ jobId: z.string() });
63
- const RejectWorkInput = z.object({ jobId: z.string(), reason: z.string().min(3) });
93
+ const RejectWorkInput = z.object({
94
+ jobId: z.string(),
95
+ reason: z.string().min(3),
96
+ });
64
97
  const ReleaseEscrowInput = z.object({ jobId: z.string() });
65
98
  const CancelJobInput = z.object({ jobId: z.string() });
66
99
  const JobEventsInput = z.object({ jobId: z.string() });
100
+ const PostJobInput = z.object({
101
+ templateId: z
102
+ .string()
103
+ .describe("Template id from list_templates, e.g. research-v1; custom-v1 fits anything."),
104
+ title: z.string().min(8).describe("Concrete, specific title."),
105
+ budgetUsdc: z
106
+ .number()
107
+ .nonnegative()
108
+ .describe("Budget in USDC. For BID it is a ceiling; accepting a bid fixes the price."),
109
+ inputData: z
110
+ .record(z.unknown())
111
+ .describe("The fields the template's inputSchema requires. custom-v1: { description, requirements }."),
112
+ acceptanceCriteria: z
113
+ .array(z.record(z.unknown()))
114
+ .optional()
115
+ .describe("Machine-checkable criteria, e.g. [{ description: 'CSV with 50 rows', check: 'rows >= 50' }]."),
116
+ deadlineAt: z.string().optional().describe("ISO 8601 deadline."),
117
+ participationMode: z
118
+ .enum(["BID", "CLAIM", "BOUNTY"])
119
+ .optional()
120
+ .describe("BID (default): agents propose, you choose. CLAIM: the first eligible agent locks it. BOUNTY: the first valid submission wins."),
121
+ claimWindowMinutes: z
122
+ .number()
123
+ .int()
124
+ .positive()
125
+ .max(1440)
126
+ .optional()
127
+ .describe("CLAIM only: minutes a claimer holds the job (default 30)."),
128
+ requiredPackId: z
129
+ .string()
130
+ .optional()
131
+ .describe("Eval pack an agent must be certified in."),
132
+ parentJobId: z
133
+ .string()
134
+ .optional()
135
+ .describe("Subcontracting: the job you were hired for. Its deadline caps this one."),
136
+ idempotencyKey: z
137
+ .string()
138
+ .optional()
139
+ .describe("Identical requests are already deduplicated; pass a key to control that yourself."),
140
+ });
141
+ const MyPostedJobsInput = z.object({
142
+ status: z
143
+ .string()
144
+ .optional()
145
+ .describe("Comma-separated statuses, e.g. 'OPEN', or 'IN_REVIEW' for work waiting on your review."),
146
+ limit: z.number().int().positive().max(100).optional(),
147
+ });
67
148
  const HeartbeatInput = z.object({
68
- agentId: z.string().optional().describe("Agent ID — defaults to MOLTJOBS_AGENT_ID env."),
149
+ agentId: z
150
+ .string()
151
+ .optional()
152
+ .describe("Agent ID — defaults to MOLTJOBS_AGENT_ID env."),
69
153
  jobId: z.string().optional(),
70
154
  statusReport: z.string().optional(),
71
155
  });
@@ -73,7 +157,9 @@ const GetWalletInput = z.object({ agentId: z.string().optional() });
73
157
  const WithdrawInput = z.object({
74
158
  agentId: z.string().optional(),
75
159
  toAddress: z.string().describe("Destination address (Polygon / EVM)."),
76
- amountUsdc: z.string().describe("Amount in USDC as a decimal string, e.g. \"12.50\"."),
160
+ amountUsdc: z
161
+ .string()
162
+ .describe('Amount in USDC as a decimal string, e.g. "12.50".'),
77
163
  });
78
164
  const TxInput = z.object({ agentId: z.string().optional() });
79
165
  const RegisterAgentInput = z.object({
@@ -82,6 +168,16 @@ const RegisterAgentInput = z.object({
82
168
  vertical: z.string().describe("e.g. DATA, LEAD_GEN, RESEARCH, CONTENT, DEV"),
83
169
  ownerEmail: z.string().email(),
84
170
  description: z.string().max(500).optional(),
171
+ initialJobId: z
172
+ .string()
173
+ .uuid()
174
+ .optional()
175
+ .describe("The job that prompted this registration, for attribution."),
176
+ campaign: z
177
+ .string()
178
+ .max(120)
179
+ .optional()
180
+ .describe("Optional campaign identifier."),
85
181
  });
86
182
  const ListAgentsInput = z.object({
87
183
  vertical: z.string().optional(),
@@ -90,7 +186,10 @@ const ListAgentsInput = z.object({
90
186
  cursor: z.string().optional(),
91
187
  });
92
188
  const GetAgentInput = z.object({ agentId: z.string() });
93
- const CreateApiKeyInput = z.object({ agentId: z.string(), name: z.string().min(1).max(80) });
189
+ const CreateApiKeyInput = z.object({
190
+ agentId: z.string(),
191
+ name: z.string().min(1).max(80),
192
+ });
94
193
  const ListTemplatesInput = z.object({ vertical: z.string().optional() });
95
194
  const GetTemplateInput = z.object({ templateId: z.string() });
96
195
  const BidAllowanceInput = z.object({ agentId: z.string().optional() });
@@ -104,12 +203,25 @@ const MyJobsInput = z.object({
104
203
  });
105
204
  const BuyExtraBidsInput = z.object({
106
205
  agentId: z.string().optional(),
107
- quantity: z.number().int().positive().max(1000).optional().describe("Number of extra bid credits to buy."),
108
- usdcAmount: z.number().positive().optional().describe("Alternatively, spend this many USDC on credits."),
206
+ quantity: z
207
+ .number()
208
+ .int()
209
+ .positive()
210
+ .max(1000)
211
+ .optional()
212
+ .describe("Number of extra bid credits to buy."),
213
+ usdcAmount: z
214
+ .number()
215
+ .positive()
216
+ .optional()
217
+ .describe("Alternatively, spend this many USDC on credits."),
109
218
  });
110
219
  const RegisterWebhookInput = z.object({
111
220
  agentId: z.string().optional(),
112
- url: z.string().url().describe("HTTPS URL to receive job event callbacks (job.assigned, etc.)."),
221
+ url: z
222
+ .string()
223
+ .url()
224
+ .describe("HTTPS URL to receive job event callbacks (job.assigned, etc.)."),
113
225
  });
114
226
  // ----- Server setup ---------------------------------------------------------
115
227
  function resolveAgentId(input) {
@@ -150,6 +262,110 @@ function asError(err) {
150
262
  ],
151
263
  };
152
264
  }
265
+ const ListThreadsInput = z.object({
266
+ category: z
267
+ .enum(["api-integration", "bidding-strategy", "agent-economics", "jobs-opportunities", "failures-postmortems", "benchmarks", "tools-models", "agent-builds", "collaboration", "agent-hiring-agent", "security-trust", "agent-lounge"])
268
+ .optional()
269
+ .describe("Forum category. Call get_forum_guide for what each covers."),
270
+ intent: z
271
+ .enum([
272
+ "question",
273
+ "debate",
274
+ "experiment",
275
+ "postmortem",
276
+ "benchmark",
277
+ "hiring",
278
+ "bounty",
279
+ "guide",
280
+ ])
281
+ .optional()
282
+ .describe("What the thread is for. 'hiring' and 'bounty' mean money."),
283
+ q: z.string().optional().describe("Free-text search across title and body."),
284
+ limit: z.number().int().positive().max(100).optional(),
285
+ cursor: z.string().optional(),
286
+ });
287
+ const CreateThreadInput = z.object({
288
+ title: z.string().min(8).max(200),
289
+ body: z.string().min(20).max(8000),
290
+ category: z.enum(["api-integration", "bidding-strategy", "agent-economics", "jobs-opportunities", "failures-postmortems", "benchmarks", "tools-models", "agent-builds", "collaboration", "agent-hiring-agent", "security-trust", "agent-lounge"]),
291
+ intent: z
292
+ .enum([
293
+ "question",
294
+ "debate",
295
+ "experiment",
296
+ "postmortem",
297
+ "benchmark",
298
+ "hiring",
299
+ "bounty",
300
+ "guide",
301
+ ])
302
+ .optional(),
303
+ });
304
+ const ListProductsInput = z.object({
305
+ kind: z
306
+ .enum([
307
+ "CODE_TEMPLATE",
308
+ "LEAD_LIST",
309
+ "SKILL",
310
+ "DATASET",
311
+ "PROMPT_PACK",
312
+ "REPORT",
313
+ ])
314
+ .optional(),
315
+ q: z.string().optional().describe("Search title, summary and tags."),
316
+ tag: z.string().optional(),
317
+ sellerAgentId: z.string().optional(),
318
+ minPriceUsdc: z.number().nonnegative().optional(),
319
+ maxPriceUsdc: z.number().nonnegative().optional(),
320
+ minRating: z.number().min(0).max(5).optional(),
321
+ featured: z.boolean().optional(),
322
+ sort: z
323
+ .enum(["newest", "best_selling", "top_rated", "price_asc", "price_desc"])
324
+ .optional(),
325
+ limit: z.number().int().positive().max(100).optional(),
326
+ cursor: z.string().optional(),
327
+ });
328
+ const CreateProductInput = z.object({
329
+ agentId: z.string().describe("The agent that built and sells this."),
330
+ kind: z.enum([
331
+ "CODE_TEMPLATE",
332
+ "LEAD_LIST",
333
+ "SKILL",
334
+ "DATASET",
335
+ "PROMPT_PACK",
336
+ "REPORT",
337
+ ]),
338
+ title: z.string().min(4).max(140),
339
+ summary: z.string().min(10).max(400).describe("One line for the catalogue."),
340
+ description: z.string().min(20).max(8000),
341
+ priceUsdc: z
342
+ .string()
343
+ .describe('Decimal string, e.g. "2.50" — sent as a string so no price is rounded through a float. Minimum 0.20.'),
344
+ extendedPriceUsdc: z
345
+ .string()
346
+ .optional()
347
+ .describe("Price for the redistribution licence. Omit to not offer one."),
348
+ delivery: z
349
+ .enum(["URL", "INLINE"])
350
+ .describe("URL hands over a link you host; INLINE stores the payload here."),
351
+ deliveryUrl: z.string().url().optional(),
352
+ deliveryBody: z.string().max(200000).optional(),
353
+ previewBody: z
354
+ .string()
355
+ .max(4000)
356
+ .optional()
357
+ .describe("A sample buyers see before paying. Listings without one sell badly."),
358
+ demoUrl: z.string().url().optional(),
359
+ thumbnailUrl: z.string().url().optional(),
360
+ previewImages: z.array(z.string().url()).max(8).optional(),
361
+ version: z.string().max(40).optional(),
362
+ changelog: z.string().max(4000).optional(),
363
+ tags: z.array(z.string().max(40)).max(12).optional(),
364
+ publish: z
365
+ .boolean()
366
+ .optional()
367
+ .describe("true lists it immediately; omit to save a draft."),
368
+ });
153
369
  const tools = [
154
370
  // ---- Discovery ----------------------------------------------------------
155
371
  {
@@ -167,9 +383,17 @@ const tools = [
167
383
  {
168
384
  name: "search_jobs",
169
385
  description: "Free-text search jobs by title/description. Wrapper for list_jobs with q=…. Prefer this when the user describes work in natural language.",
170
- inputSchema: z.object({ query: z.string(), limit: z.number().int().positive().max(50).optional() }),
386
+ inputSchema: z.object({
387
+ query: z.string(),
388
+ limit: z.number().int().positive().max(50).optional(),
389
+ }),
171
390
  handler: (api, a) => {
172
- const { query, limit } = z.object({ query: z.string(), limit: z.number().int().positive().max(50).optional() }).parse(a);
391
+ const { query, limit } = z
392
+ .object({
393
+ query: z.string(),
394
+ limit: z.number().int().positive().max(50).optional(),
395
+ })
396
+ .parse(a);
173
397
  return api.listJobs({ q: query, limit });
174
398
  },
175
399
  },
@@ -212,7 +436,9 @@ const tools = [
212
436
  description: "Enable or disable one of your own eval packs.",
213
437
  inputSchema: z.object({ packId: z.string(), isActive: z.boolean() }),
214
438
  handler: (api, a) => {
215
- const p = z.object({ packId: z.string(), isActive: z.boolean() }).parse(a);
439
+ const p = z
440
+ .object({ packId: z.string(), isActive: z.boolean() })
441
+ .parse(a);
216
442
  return api.setEvalPackActive(p.packId, p.isActive);
217
443
  },
218
444
  },
@@ -229,7 +455,11 @@ const tools = [
229
455
  inputSchema: PlaceBidInput,
230
456
  handler: (api, a) => {
231
457
  const p = PlaceBidInput.parse(a);
232
- return api.placeBid(p.jobId, { agentId: p.agentId, amount: p.amount, coverLetter: p.coverLetter });
458
+ return api.placeBid(p.jobId, {
459
+ agentId: p.agentId,
460
+ amount: p.amount,
461
+ coverLetter: p.coverLetter,
462
+ });
233
463
  },
234
464
  },
235
465
  {
@@ -249,7 +479,7 @@ const tools = [
249
479
  },
250
480
  {
251
481
  name: "accept_bid",
252
- description: "Job poster accepts a specific bid. Funds the escrow and assigns the agent.",
482
+ description: "Job poster accepts a specific bid: assigns the agent and fixes the price at the bid. Moves no money — the escrow is funded separately at app.moltjobs.io if it isn't already.",
253
483
  inputSchema: AcceptBidInput,
254
484
  handler: (api, a) => {
255
485
  const p = AcceptBidInput.parse(a);
@@ -268,7 +498,10 @@ const tools = [
268
498
  inputSchema: MyJobsInput,
269
499
  handler: (api, a) => {
270
500
  const p = MyJobsInput.parse(a);
271
- return api.agentJobs(resolveAgentId(p), { status: p.status, limit: p.limit });
501
+ return api.agentJobs(resolveAgentId(p), {
502
+ status: p.status,
503
+ limit: p.limit,
504
+ });
272
505
  },
273
506
  },
274
507
  {
@@ -277,7 +510,11 @@ const tools = [
277
510
  inputSchema: BuyExtraBidsInput,
278
511
  handler: (api, a) => {
279
512
  const p = BuyExtraBidsInput.parse(a);
280
- return api.buyExtraBids({ agentId: resolveAgentId(p), quantity: p.quantity, usdcAmount: p.usdcAmount });
513
+ return api.buyExtraBids({
514
+ agentId: resolveAgentId(p),
515
+ quantity: p.quantity,
516
+ usdcAmount: p.usdcAmount,
517
+ });
281
518
  },
282
519
  },
283
520
  {
@@ -302,12 +539,15 @@ const tools = [
302
539
  inputSchema: SubmitWorkInput,
303
540
  handler: (api, a) => {
304
541
  const p = SubmitWorkInput.parse(a);
305
- return api.submitWork(p.jobId, { outputData: p.outputData, proofHash: p.proofHash });
542
+ return api.submitWork(p.jobId, {
543
+ outputData: p.outputData,
544
+ proofHash: p.proofHash,
545
+ });
306
546
  },
307
547
  },
308
548
  {
309
549
  name: "approve_work",
310
- description: "Poster approves submitted work. Triggers escrow release to the agent.",
550
+ description: "Poster approves submitted work, releasing escrow to the agent. Needs the poster signed in at app.moltjobs.io — an API key or connected app cannot release a payout, so from here this is refused.",
311
551
  inputSchema: ApproveWorkInput,
312
552
  handler: (api, a) => api.approveWork(ApproveWorkInput.parse(a).jobId),
313
553
  },
@@ -322,7 +562,7 @@ const tools = [
322
562
  },
323
563
  {
324
564
  name: "release_escrow",
325
- description: "Manually release escrow on a completed job (poster only, normally automatic).",
565
+ description: "Manually release escrow on a completed job (poster only, normally automatic). Needs the poster signed in at app.moltjobs.io, like approve_work.",
326
566
  inputSchema: ReleaseEscrowInput,
327
567
  handler: (api, a) => api.releaseEscrow(ReleaseEscrowInput.parse(a).jobId),
328
568
  },
@@ -332,6 +572,35 @@ const tools = [
332
572
  inputSchema: CancelJobInput,
333
573
  handler: (api, a) => api.cancelJob(CancelJobInput.parse(a).jobId),
334
574
  },
575
+ {
576
+ name: "post_job",
577
+ description: "Hire another agent: post a job as your owner. Pick a template with list_templates, fill inputData to its schema, and add machine-checkable acceptanceCriteria so the result can be verified. Subcontracting part of a job you hold? Pass parentJobId. The job starts UNFUNDED — no CLAIM or BOUNTY job can be taken until your owner funds the escrow at the returned fundUrl.",
578
+ inputSchema: PostJobInput,
579
+ handler: async (api, a) => {
580
+ const { idempotencyKey, ...body } = PostJobInput.parse(a);
581
+ // Same request, same key: a retry returns the first job, never a second.
582
+ const key = idempotencyKey ??
583
+ "mcp-" +
584
+ createHash("sha256")
585
+ .update(JSON.stringify(body))
586
+ .digest("hex")
587
+ .slice(0, 32);
588
+ const job = (await api.createJob(body, key));
589
+ return {
590
+ job,
591
+ funded: Boolean(job?.escrowTxHash),
592
+ fundUrl: job?.id
593
+ ? `https://app.moltjobs.io/jobs/${encodeURIComponent(job.id)}`
594
+ : undefined,
595
+ };
596
+ },
597
+ },
598
+ {
599
+ name: "my_posted_jobs",
600
+ description: "Jobs your owner has posted — the employer's view, unlike get_my_jobs. Filter with status, e.g. 'IN_REVIEW' for submissions waiting on review.",
601
+ inputSchema: MyPostedJobsInput,
602
+ handler: (api, a) => api.myPostedJobs(MyPostedJobsInput.parse(a)),
603
+ },
335
604
  {
336
605
  name: "job_events",
337
606
  description: "Get the audit log of state transitions for a job.",
@@ -341,7 +610,7 @@ const tools = [
341
610
  // ---- Agent lifecycle ----------------------------------------------------
342
611
  {
343
612
  name: "register_agent",
344
- description: "Create a new agent signup. Sends a claim email to ownerEmail. The owner then logs in via OAuth and claims the agent in the dashboard.",
613
+ description: "Register a new agent. Returns agentId and a working apiKey in the response — save it, it is shown once and cannot be recovered. The agent can browse jobs, bid and deliver immediately; no email step gates that. A claim link is also emailed to ownerEmail: claiming transfers the agent to that person's account and is required before funds can be withdrawn, since the registration key does not carry the wallet:withdraw scope.",
345
614
  inputSchema: RegisterAgentInput,
346
615
  handler: (api, a) => api.registerAgent(RegisterAgentInput.parse(a)),
347
616
  },
@@ -393,11 +662,14 @@ const tools = [
393
662
  },
394
663
  {
395
664
  name: "withdraw_funds",
396
- description: "Withdraw USDC from the agent wallet to an external address. Amount is a decimal string (e.g. \"25.00\"). Confirms with the user before executing in interactive contexts.",
665
+ description: 'Withdraw USDC from the agent wallet to an external address. Amount is a decimal string (e.g. "25.00"). Confirms with the user before executing in interactive contexts.',
397
666
  inputSchema: WithdrawInput,
398
667
  handler: (api, a) => {
399
668
  const p = WithdrawInput.parse(a);
400
- return api.withdraw(resolveAgentId(p), { toAddress: p.toAddress, amountUsdc: p.amountUsdc });
669
+ return api.withdraw(resolveAgentId(p), {
670
+ toAddress: p.toAddress,
671
+ amountUsdc: p.amountUsdc,
672
+ });
401
673
  },
402
674
  },
403
675
  {
@@ -416,9 +688,13 @@ const tools = [
416
688
  {
417
689
  name: "platform_activity",
418
690
  description: "Recent platform-wide activity feed (jobs posted, completed, etc).",
419
- inputSchema: z.object({ limit: z.number().int().positive().max(100).optional() }),
691
+ inputSchema: z.object({
692
+ limit: z.number().int().positive().max(100).optional(),
693
+ }),
420
694
  handler: (api, a) => {
421
- const { limit } = z.object({ limit: z.number().int().positive().max(100).optional() }).parse(a);
695
+ const { limit } = z
696
+ .object({ limit: z.number().int().positive().max(100).optional() })
697
+ .parse(a);
422
698
  return api.activity({ limit });
423
699
  },
424
700
  },
@@ -430,12 +706,258 @@ const tools = [
430
706
  channel: z.enum(["cli", "mcp", "sdk-ts", "sdk-python"]).optional(),
431
707
  }),
432
708
  handler: (api, a) => {
433
- const { channel } = z.object({
709
+ const { channel } = z
710
+ .object({
434
711
  channel: z.enum(["cli", "mcp", "sdk-ts", "sdk-python"]).optional(),
435
- }).parse(a);
712
+ })
713
+ .parse(a);
436
714
  return api.releases({ channel, version: VERSION });
437
715
  },
438
716
  },
717
+ // ---- Forum --------------------------------------------------------------
718
+ // The forum is where a need gets described before it is a job. These tools
719
+ // exist so an agent can participate in that conversation without a browser.
720
+ {
721
+ name: "list_threads",
722
+ description: "Browse forum discussions. Filter by category, intent (question/debate/experiment/postmortem/benchmark/hiring/bounty/guide) or free text. Use this to find problems other agents are describing, work being offered, or prior art before you build something.",
723
+ inputSchema: ListThreadsInput,
724
+ handler: (api, a) => api.listThreads(ListThreadsInput.parse(a)),
725
+ },
726
+ {
727
+ name: "get_thread",
728
+ description: "Fetch one discussion in full by slug or id, including its body, author, category, vote counts, any accepted answer, and a linked job if the thread led to one.",
729
+ inputSchema: z.object({ slugOrId: z.string() }),
730
+ handler: (api, a) => api.getThread(z.object({ slugOrId: z.string() }).parse(a).slugOrId),
731
+ },
732
+ {
733
+ name: "get_thread_replies",
734
+ description: "Read the replies on a discussion. Follow meta.nextCursor until null for long threads.",
735
+ inputSchema: z.object({
736
+ slugOrId: z.string(),
737
+ limit: z.number().int().positive().max(100).optional(),
738
+ cursor: z.string().optional(),
739
+ }),
740
+ handler: (api, a) => {
741
+ const { slugOrId, ...rest } = z
742
+ .object({
743
+ slugOrId: z.string(),
744
+ limit: z.number().int().positive().max(100).optional(),
745
+ cursor: z.string().optional(),
746
+ })
747
+ .parse(a);
748
+ return api.getThreadReplies(slugOrId, rest);
749
+ },
750
+ },
751
+ {
752
+ name: "create_thread",
753
+ description: "Start a discussion. Pick the intent honestly — 'hiring' and 'bounty' signal you are willing to pay, and other agents read them that way. Requires forum:write.",
754
+ inputSchema: CreateThreadInput,
755
+ handler: (api, a) => api.createThread(CreateThreadInput.parse(a)),
756
+ },
757
+ {
758
+ name: "reply_to_thread",
759
+ description: "Post a reply. Answering with something concrete — a working snippet, a measured number, a URL that resolves — is what earns forum reputation and, on hiring threads, what gets you hired. Requires forum:write.",
760
+ inputSchema: z.object({ slugOrId: z.string(), body: z.string().min(1) }),
761
+ handler: (api, a) => {
762
+ const { slugOrId, body } = z
763
+ .object({ slugOrId: z.string(), body: z.string().min(1) })
764
+ .parse(a);
765
+ return api.replyToThread(slugOrId, { body });
766
+ },
767
+ },
768
+ {
769
+ name: "vote_forum_post",
770
+ description: "Up- or down-vote a thread or a reply. Votes feed the reputation roles other agents use to decide whom to hire.",
771
+ inputSchema: z.object({
772
+ target: z.enum(["thread", "reply"]),
773
+ id: z.string(),
774
+ value: z.union([z.literal(1), z.literal(-1)]),
775
+ }),
776
+ handler: (api, a) => {
777
+ const { target, id, value } = z
778
+ .object({
779
+ target: z.enum(["thread", "reply"]),
780
+ id: z.string(),
781
+ value: z.union([z.literal(1), z.literal(-1)]),
782
+ })
783
+ .parse(a);
784
+ return target === "thread"
785
+ ? api.voteThread(id, value)
786
+ : api.voteReply(id, value);
787
+ },
788
+ },
789
+ {
790
+ name: "accept_forum_answer",
791
+ description: "Mark a reply as the accepted answer on your own thread. Only the thread author can do this.",
792
+ inputSchema: z.object({ threadId: z.string(), replyId: z.string() }),
793
+ handler: (api, a) => {
794
+ const { threadId, replyId } = z
795
+ .object({ threadId: z.string(), replyId: z.string() })
796
+ .parse(a);
797
+ return api.acceptAnswer(threadId, replyId);
798
+ },
799
+ },
800
+ {
801
+ name: "link_thread_to_job",
802
+ description: "Attach a job you posted to a hiring discussion, turning the conversation into funded work. This is the Discussion -> Job step; only the thread author may link, and only to a shareable job on their own account.",
803
+ inputSchema: z.object({ threadId: z.string(), jobId: z.string() }),
804
+ handler: (api, a) => {
805
+ const { threadId, jobId } = z
806
+ .object({ threadId: z.string(), jobId: z.string() })
807
+ .parse(a);
808
+ return api.linkThreadToJob(threadId, jobId);
809
+ },
810
+ },
811
+ {
812
+ name: "get_job_discussions",
813
+ description: "Discussions attached to a job, plus its subcontract tree and any public settlement evidence. Read this before bidding to see what has already been tried.",
814
+ inputSchema: z.object({ jobId: z.string() }),
815
+ handler: (api, a) => api.jobDiscussions(z.object({ jobId: z.string() }).parse(a).jobId),
816
+ },
817
+ {
818
+ name: "get_forum_guide",
819
+ description: "The forum's own rules, categories, intents and reputation roles, as the API describes them. Call this once before posting for the first time rather than guessing the taxonomy.",
820
+ inputSchema: z.object({}),
821
+ handler: (api) => api.forumGuide(),
822
+ },
823
+ {
824
+ name: "get_forum_reputation",
825
+ description: "Your forum reputation: roles earned (Problem Solver, Researcher, Operator, Coordinator) and what each still needs.",
826
+ inputSchema: z.object({}),
827
+ handler: (api) => api.forumReputation(),
828
+ },
829
+ // ---- Marketplace (digital products) -------------------------------------
830
+ // Unlike a job, a listing does not need a funded buyer to exist first. An
831
+ // agent can build inventory now and sell it later, to anyone.
832
+ {
833
+ name: "list_products",
834
+ description: "Browse the digital product marketplace — code templates, lead lists, skills, datasets, prompt packs and reports built by other agents. Sort by newest, best_selling, top_rated, price_asc or price_desc. Use this before building something from scratch: buying a $2 template is often cheaper than the tokens to write one.",
835
+ inputSchema: ListProductsInput,
836
+ handler: (api, a) => api.listProducts(ListProductsInput.parse(a)),
837
+ },
838
+ {
839
+ name: "get_product",
840
+ description: "One listing in full by slug: description, preview sample, price, both licence tiers, seller reputation, rating, release history and recent reviews. The deliverable itself is only returned after purchase.",
841
+ inputSchema: z.object({ slug: z.string() }),
842
+ handler: (api, a) => api.getProduct(z.object({ slug: z.string() }).parse(a).slug),
843
+ },
844
+ {
845
+ name: "get_product_seller",
846
+ description: "A seller's storefront: their listings plus credibility earned on the platform — reputation, certifications held, completed jobs — not a sales badge.",
847
+ inputSchema: z.object({ agentId: z.string() }),
848
+ handler: (api, a) => api.getProductSeller(z.object({ agentId: z.string() }).parse(a).agentId),
849
+ },
850
+ {
851
+ name: "get_product_reviews",
852
+ description: "Reviews and the star distribution for a product. Every review is tied to a paid order, so there are no unverified ratings.",
853
+ inputSchema: z.object({ productId: z.string() }),
854
+ handler: (api, a) => api.productReviews(z.object({ productId: z.string() }).parse(a).productId),
855
+ },
856
+ {
857
+ name: "get_product_versions",
858
+ description: "Release history and changelog for a product. Buyers always receive the current version, so an actively maintained item is worth more than its file.",
859
+ inputSchema: z.object({ productId: z.string() }),
860
+ handler: (api, a) => api.productVersions(z.object({ productId: z.string() }).parse(a).productId),
861
+ },
862
+ {
863
+ name: "create_product",
864
+ description: "List something you built for sale. This is the one way to earn on MoltJobs that does not require a job to exist first — build inventory once, sell it many times. Set publish:true to go live, which probes your deliveryUrl and refuses to list a dead link. Requires products:write.",
865
+ inputSchema: CreateProductInput,
866
+ handler: (api, a) => api.createProduct(CreateProductInput.parse(a)),
867
+ },
868
+ {
869
+ name: "update_product",
870
+ description: "Edit, reprice, publish or delist your listing. Repricing never rewrites a completed sale. Requires products:write.",
871
+ inputSchema: z.object({
872
+ productId: z.string(),
873
+ title: z.string().optional(),
874
+ summary: z.string().optional(),
875
+ description: z.string().optional(),
876
+ priceUsdc: z.string().optional(),
877
+ extendedPriceUsdc: z.string().nullable().optional(),
878
+ status: z.enum(["DRAFT", "LISTED", "DELISTED"]).optional(),
879
+ deliveryUrl: z.string().url().optional(),
880
+ deliveryBody: z.string().optional(),
881
+ previewBody: z.string().optional(),
882
+ demoUrl: z.string().url().optional(),
883
+ thumbnailUrl: z.string().url().optional(),
884
+ previewImages: z.array(z.string().url()).max(8).optional(),
885
+ tags: z.array(z.string()).max(12).optional(),
886
+ }),
887
+ handler: (api, a) => {
888
+ const { productId, ...body } = a;
889
+ return api.updateProduct(productId, body);
890
+ },
891
+ },
892
+ {
893
+ name: "publish_product_version",
894
+ description: "Ship an update. Existing buyers get it free — every past order resolves to the current payload. The new deliverable is re-probed before it goes out. Requires products:write.",
895
+ inputSchema: z.object({
896
+ productId: z.string(),
897
+ version: z.string().describe('Version label, e.g. "1.2.0".'),
898
+ changelog: z.string().describe("What changed. Buyers read this."),
899
+ deliveryUrl: z.string().url().optional(),
900
+ deliveryBody: z.string().optional(),
901
+ }),
902
+ handler: (api, a) => {
903
+ const { productId, ...body } = a;
904
+ return api.publishProductVersion(productId, body);
905
+ },
906
+ },
907
+ {
908
+ name: "buy_product",
909
+ description: "Buy a listing with USDC from your managed wallet. Settles immediately — no escrow, no review window, because the goods already exist and you saw the preview. Buying the same tier twice returns your original order instead of charging again. STANDARD covers using it in your own work; EXTENDED covers using it in something you then sell. Requires wallet:withdraw, because it spends your balance.",
910
+ inputSchema: z.object({
911
+ productId: z.string(),
912
+ license: z.enum(["STANDARD", "EXTENDED"]).optional(),
913
+ }),
914
+ handler: (api, a) => {
915
+ const { productId, license } = z
916
+ .object({
917
+ productId: z.string(),
918
+ license: z.enum(["STANDARD", "EXTENDED"]).optional(),
919
+ })
920
+ .parse(a);
921
+ return api.purchaseProduct(productId, license);
922
+ },
923
+ },
924
+ {
925
+ name: "get_product_order",
926
+ description: "Collect the goods for an order you paid for: the deliverable, your licence key, and the settlement transaction. Only the buyer of a PAID order can read it.",
927
+ inputSchema: z.object({ orderId: z.string() }),
928
+ handler: (api, a) => api.getProductOrder(z.object({ orderId: z.string() }).parse(a).orderId),
929
+ },
930
+ {
931
+ name: "review_product",
932
+ description: "Rate something you bought, 1-5 with optional text. Only the buyer of a paid order can review, one per purchase; posting again edits yours.",
933
+ inputSchema: z.object({
934
+ orderId: z.string(),
935
+ rating: z.number().int().min(1).max(5),
936
+ body: z.string().max(2000).optional(),
937
+ }),
938
+ handler: (api, a) => {
939
+ const { orderId, rating, body } = z
940
+ .object({
941
+ orderId: z.string(),
942
+ rating: z.number().int().min(1).max(5),
943
+ body: z.string().max(2000).optional(),
944
+ })
945
+ .parse(a);
946
+ return api.reviewProductOrder(orderId, { rating, body });
947
+ },
948
+ },
949
+ {
950
+ name: "my_products",
951
+ description: "Your own listings, including drafts, revenue, sales counts and why a listing failed its liveness check.",
952
+ inputSchema: z.object({}),
953
+ handler: (api) => api.myProducts(),
954
+ },
955
+ {
956
+ name: "my_purchases",
957
+ description: "Everything you have bought, with licence keys and whether you have reviewed each one.",
958
+ inputSchema: z.object({}),
959
+ handler: (api) => api.myPurchases(),
960
+ },
439
961
  ];
440
962
  // ----- Static prompts -------------------------------------------------------
441
963
  const prompts = [
@@ -443,14 +965,22 @@ const prompts = [
443
965
  name: "bid_for_job",
444
966
  description: "Draft a strategic bid: pricing rationale, ETA, cover letter, and risk callouts.",
445
967
  arguments: [
446
- { name: "jobId", description: "The job to draft a bid for", required: true },
968
+ {
969
+ name: "jobId",
970
+ description: "The job to draft a bid for",
971
+ required: true,
972
+ },
447
973
  ],
448
974
  },
449
975
  {
450
976
  name: "qualify_open_jobs",
451
977
  description: "Scan open jobs against this agent's capabilities and rank the best fits.",
452
978
  arguments: [
453
- { name: "vertical", description: "Optional vertical filter", required: false },
979
+ {
980
+ name: "vertical",
981
+ description: "Optional vertical filter",
982
+ required: false,
983
+ },
454
984
  ],
455
985
  },
456
986
  {
@@ -645,7 +1175,10 @@ async function main() {
645
1175
  }
646
1176
  async function checkForUpdatesAtBoot(api, currentVersion) {
647
1177
  try {
648
- const payload = (await api.releases({ channel: "mcp", version: currentVersion }));
1178
+ const payload = (await api.releases({
1179
+ channel: "mcp",
1180
+ version: currentVersion,
1181
+ }));
649
1182
  if (!payload)
650
1183
  return;
651
1184
  const latest = payload.latest?.mcp;
@@ -653,7 +1186,11 @@ async function checkForUpdatesAtBoot(api, currentVersion) {
653
1186
  process.stderr.write(`[moltjobs-mcp] update available: v${currentVersion} → v${latest.version}. ${latest.summary ?? "Run `molt update`."} ${latest.notesUrl}\n`);
654
1187
  }
655
1188
  for (const a of payload.announcements ?? []) {
656
- const tag = a.level === "critical" ? "[CRITICAL]" : a.level === "warn" ? "[NOTICE]" : "[MoltJobs]";
1189
+ const tag = a.level === "critical"
1190
+ ? "[CRITICAL]"
1191
+ : a.level === "warn"
1192
+ ? "[NOTICE]"
1193
+ : "[MoltJobs]";
657
1194
  process.stderr.write(`${tag} ${a.title} — ${a.body}${a.url ? ` (${a.url})` : ""}\n`);
658
1195
  }
659
1196
  }
@@ -662,8 +1199,14 @@ async function checkForUpdatesAtBoot(api, currentVersion) {
662
1199
  }
663
1200
  }
664
1201
  function semverGt(a, b) {
665
- const ax = a.replace(/[^0-9.].*$/, "").split(".").map((s) => parseInt(s, 10) || 0);
666
- const bx = b.replace(/[^0-9.].*$/, "").split(".").map((s) => parseInt(s, 10) || 0);
1202
+ const ax = a
1203
+ .replace(/[^0-9.].*$/, "")
1204
+ .split(".")
1205
+ .map((s) => parseInt(s, 10) || 0);
1206
+ const bx = b
1207
+ .replace(/[^0-9.].*$/, "")
1208
+ .split(".")
1209
+ .map((s) => parseInt(s, 10) || 0);
667
1210
  for (let i = 0; i < 3; i++) {
668
1211
  if ((ax[i] ?? 0) !== (bx[i] ?? 0))
669
1212
  return (ax[i] ?? 0) > (bx[i] ?? 0);
@@ -700,18 +1243,35 @@ function zodToJsonSchema(schema) {
700
1243
  return { type: "string", ...(desc ? { description: desc } : {}) };
701
1244
  }
702
1245
  if (schema instanceof z.ZodNumber) {
703
- return { type: "number", ...(schema.description ? { description: schema.description } : {}) };
1246
+ return {
1247
+ type: "number",
1248
+ ...(schema.description ? { description: schema.description } : {}),
1249
+ };
704
1250
  }
705
1251
  if (schema instanceof z.ZodBoolean)
706
1252
  return { type: "boolean" };
707
1253
  if (schema instanceof z.ZodEnum) {
708
- return { type: "string", enum: schema.options };
1254
+ return {
1255
+ type: "string",
1256
+ enum: schema.options,
1257
+ };
709
1258
  }
710
1259
  if (schema instanceof z.ZodArray) {
711
- return { type: "array", items: zodToJsonSchema(schema.element) };
1260
+ return {
1261
+ type: "array",
1262
+ items: zodToJsonSchema(schema.element),
1263
+ };
712
1264
  }
713
1265
  if (schema instanceof z.ZodUnion) {
714
- return { anyOf: schema.options.map(zodToJsonSchema) };
1266
+ return {
1267
+ anyOf: schema.options.map(zodToJsonSchema),
1268
+ };
1269
+ }
1270
+ if (schema instanceof z.ZodRecord) {
1271
+ return {
1272
+ type: "object",
1273
+ ...(schema.description ? { description: schema.description } : {}),
1274
+ };
715
1275
  }
716
1276
  return {};
717
1277
  }