@elisym/mcp 0.8.19 → 0.9.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/README.md CHANGED
@@ -5,7 +5,6 @@
5
5
  [![Docker](https://img.shields.io/badge/ghcr.io-elisymlabs%2Fmcp-blue)](https://github.com/elisymlabs/elisym/pkgs/container/mcp)
6
6
  [![MCP Registry](https://img.shields.io/badge/MCP-Registry-5865F2)](https://registry.modelcontextprotocol.io/v0/servers?search=elisym)
7
7
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](../../LICENSE)
8
- [![elisym MCP server](https://glama.ai/mcp/servers/elisymlabs/elisym/badges/score.svg)](https://glama.ai/mcp/servers/elisymlabs/elisym)
9
8
 
10
9
  MCP (Model Context Protocol) server for the elisym agent network - open infrastructure for AI agents to discover, hire, and pay each other. No platform, no middleman.
11
10
 
@@ -159,6 +158,32 @@ curl -o ~/.hermes/skills/elisym-provider/SKILL.md \
159
158
  https://raw.githubusercontent.com/elisymlabs/elisym/main/skills/elisym-provider/SKILL.md
160
159
  ```
161
160
 
161
+ ## Cost-aware job submission
162
+
163
+ Three tools submit a job and wait for the result. They are behaviorally identical from the provider's perspective - same Nostr event, same payment flow, same history record - but differ in where the job's `input` comes from. Pick based on whether the calling LLM should pay output tokens to "type" the payload into the tool call.
164
+
165
+ | Tool | `input` source | When to use |
166
+ | ------------------------------ | -------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
167
+ | `submit_and_pay_job` | inline string from the LLM | Default. Small / generated payloads where the LLM is the natural author of the input. Easiest to audit but the LLM pays output tokens for the whole input. |
168
+ | `submit_and_pay_job_from_file` | file on disk, read by MCP | Large pre-existing inputs (logs, captured output, generated docs). The LLM only emits a short tool call with `input_path`; the file content never enters its output tokens. |
169
+ | `submit_diff_review` | `git diff` run by MCP | Code review jobs. The MCP server runs `git diff` itself. Auto-detects the range (working tree vs `${main}...HEAD`); pass `base` to override. |
170
+
171
+ Examples:
172
+
173
+ ```
174
+ # inline (current default - LLM pays output tokens for the input)
175
+ Submit a job to <npub> with input "Summarize: ..."
176
+
177
+ # file-handle (cheap for large inputs)
178
+ git diff > /tmp/review.patch
179
+ Then ask: submit a job from /tmp/review.patch to <npub> capability "code-review"
180
+
181
+ # diff-specific (cheapest for code review - no temp file, no inline payload)
182
+ Ask <npub> with capability "review" to review the diff in this repo
183
+ ```
184
+
185
+ The file-handle and diff-specific variants only affect tool-call output tokens on the customer side. Provider-side compute and on-chain payment are unchanged.
186
+
162
187
  ## Security
163
188
 
164
189
  `withdraw` and `switch_agent` are gated behind opt-in flags that must be explicitly enabled per-agent:
package/dist/index.js CHANGED
@@ -5,9 +5,9 @@ import { loadGlobalConfig, writeGlobalConfig } from '@elisym/sdk/node';
5
5
  import { getBase58Encoder, getBase58Decoder, generateKeyPairSigner, createSolanaRpc, address, createSolanaRpcSubscriptions, sendAndConfirmTransactionFactory, getSignatureFromTransaction, pipe, createTransactionMessage, setTransactionMessageFeePayerSigner, setTransactionMessageLifetimeUsingBlockhash, appendTransactionMessageInstructions, signTransactionMessageWithSigners, createKeyPairSignerFromBytes, isAddress } from '@solana/kit';
6
6
  import { Command } from 'commander';
7
7
  import { generateSecretKey, nip19, getPublicKey } from 'nostr-tools';
8
- import { readFile, writeFile, rename, unlink } from 'node:fs/promises';
8
+ import { stat, readFile, writeFile, rename, unlink } from 'node:fs/promises';
9
9
  import { homedir, platform } from 'node:os';
10
- import { dirname, join } from 'node:path';
10
+ import { dirname, join, isAbsolute, resolve } from 'node:path';
11
11
  import { readFileSync } from 'node:fs';
12
12
  import { fileURLToPath } from 'node:url';
13
13
  import { Server } from '@modelcontextprotocol/sdk/server/index.js';
@@ -16,6 +16,8 @@ import { ListToolsRequestSchema, CallToolRequestSchema, ListResourcesRequestSche
16
16
  import { z, ZodError } from 'zod';
17
17
  import { zodToJsonSchema } from 'zod-to-json-schema';
18
18
  import pino from 'pino';
19
+ import { execFile } from 'node:child_process';
20
+ import { promisify } from 'node:util';
19
21
  import { randomBytes } from 'node:crypto';
20
22
  import { getTransferSolInstruction } from '@solana-program/system';
21
23
  import { findAssociatedTokenPda, TOKEN_PROGRAM_ADDRESS, getCreateAssociatedTokenIdempotentInstruction, ASSOCIATED_TOKEN_PROGRAM_ADDRESS, getTransferCheckedInstruction } from '@solana-program/token';
@@ -1001,6 +1003,129 @@ Solana: ${solanaSigner.address}
1001
1003
  }
1002
1004
  })
1003
1005
  ];
1006
+ var execFileP = promisify(execFile);
1007
+ var MAX_INPUT_PATH_LEN = 4096;
1008
+ var GIT_TIMEOUT_MS = 3e4;
1009
+ var GIT_MAX_BUFFER = MAX_INPUT_LEN * 2;
1010
+ async function readJobInputFile(inputPath) {
1011
+ if (inputPath.length > MAX_INPUT_PATH_LEN) {
1012
+ throw new Error(`input_path too long: ${inputPath.length} chars (max ${MAX_INPUT_PATH_LEN}).`);
1013
+ }
1014
+ const absPath = isAbsolute(inputPath) ? inputPath : resolve(process.cwd(), inputPath);
1015
+ let stats;
1016
+ try {
1017
+ stats = await stat(absPath);
1018
+ } catch (e) {
1019
+ const code = e.code;
1020
+ if (code === "ENOENT") {
1021
+ throw new Error(`input_path does not exist: ${absPath}`);
1022
+ }
1023
+ throw new Error(`Cannot stat input_path "${absPath}": ${e.message}`);
1024
+ }
1025
+ if (!stats.isFile()) {
1026
+ throw new Error(`input_path is not a regular file: ${absPath}`);
1027
+ }
1028
+ if (stats.size > MAX_INPUT_LEN) {
1029
+ throw new Error(
1030
+ `input_path too large: ${stats.size} bytes (max ${MAX_INPUT_LEN}). Trim the file or split the job.`
1031
+ );
1032
+ }
1033
+ const content = await readFile(absPath, "utf-8");
1034
+ if (content.length > MAX_INPUT_LEN) {
1035
+ throw new Error(
1036
+ `input_path content too long after decoding: ${content.length} chars (max ${MAX_INPUT_LEN}).`
1037
+ );
1038
+ }
1039
+ return content;
1040
+ }
1041
+ async function execGit(repoPath, args) {
1042
+ try {
1043
+ const { stdout } = await execFileP("git", args, {
1044
+ cwd: repoPath,
1045
+ timeout: GIT_TIMEOUT_MS,
1046
+ maxBuffer: GIT_MAX_BUFFER,
1047
+ env: { ...process.env, GIT_TERMINAL_PROMPT: "0" }
1048
+ });
1049
+ return stdout;
1050
+ } catch (e) {
1051
+ const err = e;
1052
+ if (err.code === "ENOENT") {
1053
+ throw new Error("git executable not found in PATH on the MCP server.");
1054
+ }
1055
+ if (err.code === "ERR_CHILD_PROCESS_STDIO_MAXBUFFER") {
1056
+ throw new Error(
1057
+ `git ${args.join(" ")} output exceeded ${GIT_MAX_BUFFER} bytes; narrow the range.`
1058
+ );
1059
+ }
1060
+ const stderr = (err.stderr ?? "").trim();
1061
+ throw new Error(`git ${args.join(" ")} failed: ${stderr || err.message}`);
1062
+ }
1063
+ }
1064
+ async function isDirty(repoPath) {
1065
+ const out = await execGit(repoPath, ["status", "--porcelain"]);
1066
+ return out.trim().length > 0;
1067
+ }
1068
+ async function detectDefaultBase(repoPath) {
1069
+ for (const candidate of ["main", "master"]) {
1070
+ try {
1071
+ await execGit(repoPath, ["rev-parse", "--verify", "--quiet", candidate]);
1072
+ return candidate;
1073
+ } catch {
1074
+ }
1075
+ }
1076
+ try {
1077
+ const out = await execGit(repoPath, ["symbolic-ref", "--short", "refs/remotes/origin/HEAD"]);
1078
+ const ref = out.trim();
1079
+ return ref.length > 0 ? ref : void 0;
1080
+ } catch {
1081
+ return void 0;
1082
+ }
1083
+ }
1084
+ async function assertGitRepo(repoPath) {
1085
+ try {
1086
+ await execGit(repoPath, ["rev-parse", "--is-inside-work-tree"]);
1087
+ } catch (e) {
1088
+ const message = e instanceof Error ? e.message : String(e);
1089
+ throw new Error(`"${repoPath}" is not inside a git work tree: ${message}`);
1090
+ }
1091
+ }
1092
+ async function computeGitDiff(repoPath, base) {
1093
+ if (repoPath.length > MAX_INPUT_PATH_LEN) {
1094
+ throw new Error(`repo_path too long: ${repoPath.length} chars (max ${MAX_INPUT_PATH_LEN}).`);
1095
+ }
1096
+ const absRepo = isAbsolute(repoPath) ? repoPath : resolve(process.cwd(), repoPath);
1097
+ await assertGitRepo(absRepo);
1098
+ let args;
1099
+ let describedRange;
1100
+ if (base) {
1101
+ args = ["diff", `${base}...HEAD`];
1102
+ describedRange = `${base}...HEAD`;
1103
+ } else if (await isDirty(absRepo)) {
1104
+ args = ["diff", "HEAD"];
1105
+ describedRange = "HEAD (working tree, uncommitted changes)";
1106
+ } else {
1107
+ const detected = await detectDefaultBase(absRepo);
1108
+ if (detected) {
1109
+ args = ["diff", `${detected}...HEAD`];
1110
+ describedRange = `${detected}...HEAD`;
1111
+ } else {
1112
+ args = ["diff", "HEAD"];
1113
+ describedRange = "HEAD (no main/master detected)";
1114
+ }
1115
+ }
1116
+ const diff = await execGit(absRepo, args);
1117
+ if (diff.trim().length === 0) {
1118
+ throw new Error(
1119
+ `No changes in range ${describedRange}. Nothing to review - commit work, pass an explicit "base", or check the repo path.`
1120
+ );
1121
+ }
1122
+ if (diff.length > MAX_INPUT_LEN) {
1123
+ throw new Error(
1124
+ `Diff for range ${describedRange} is ${diff.length} chars (max ${MAX_INPUT_LEN}). Pass a narrower "base" or split the review.`
1125
+ );
1126
+ }
1127
+ return { diff, describedRange };
1128
+ }
1004
1129
 
1005
1130
  // src/sanitize.ts
1006
1131
  var MAX_LINE_LEN = 1e4;
@@ -1349,6 +1474,28 @@ var BuyCapabilitySchema = z.object({
1349
1474
  max_price_lamports: z.number().int().optional(),
1350
1475
  timeout_secs: z.number().int().min(1).max(600).default(120)
1351
1476
  });
1477
+ var SubmitAndPayJobFromFileSchema = z.object({
1478
+ input_path: z.string().min(1).max(4096).describe(
1479
+ "Path to a regular file whose contents become the job input. Absolute or relative to the MCP server's working directory."
1480
+ ),
1481
+ provider_npub: z.string(),
1482
+ capability: z.string().min(1).max(64).default("general"),
1483
+ kind_offset: z.number().int().min(0).max(999).default(DEFAULT_KIND_OFFSET),
1484
+ timeout_secs: z.number().int().min(1).max(600).default(300),
1485
+ max_price_lamports: z.number().int().optional()
1486
+ });
1487
+ var SubmitDiffReviewSchema = z.object({
1488
+ provider_npub: z.string(),
1489
+ capability: z.string().min(1).max(64).default("review").describe('Capability tag advertised by the reviewer. Override if not "review".'),
1490
+ repo_path: z.string().min(1).max(4096).default(".").describe("Path to the git repo. Absolute or relative to the MCP server's working directory."),
1491
+ base: z.string().max(200).optional().describe(
1492
+ "Optional base ref (branch, tag, SHA). When set, diffs ${base}...HEAD. When omitted, auto-detects working-tree vs main/master/origin-HEAD."
1493
+ ),
1494
+ prompt: z.string().max(MAX_INPUT_LEN).default("").describe('Optional instructions prepended above the diff (e.g. "focus on auth flow").'),
1495
+ kind_offset: z.number().int().min(0).max(999).default(DEFAULT_KIND_OFFSET),
1496
+ timeout_secs: z.number().int().min(1).max(600).default(300),
1497
+ max_price_lamports: z.number().int().optional()
1498
+ });
1352
1499
  function providerSolanaAddress(provider, dTag) {
1353
1500
  const cards = provider.cards ?? [];
1354
1501
  const candidates = dTag ? cards.filter(
@@ -1571,6 +1718,129 @@ Payment request: ${paymentRequest}`
1571
1718
  };
1572
1719
  return { onFeedback, onResultReceived };
1573
1720
  }
1721
+ async function executeSubmitAndPay(ctx, agent, params) {
1722
+ const ping = await agent.client.ping.pingAgent(params.providerPubkey, PRE_PING_TIMEOUT_MS);
1723
+ if (!ping.online) {
1724
+ return errorResult(
1725
+ `Provider ${params.providerNpub} is offline. Run search_agents to find currently-online providers.`
1726
+ );
1727
+ }
1728
+ const providers = await agent.client.discovery.fetchAgents(agent.network);
1729
+ const provider = providers.find((a) => a.npub === params.providerNpub);
1730
+ if (!provider) {
1731
+ return errorResult(
1732
+ `Provider ${params.providerNpub} not found on ${agent.network}. Refresh discovery (e.g. search_agents) or verify the npub is correct.`
1733
+ );
1734
+ }
1735
+ const expectedRecipient = providerSolanaAddress(provider, params.dTag);
1736
+ if (agent.solanaKeypair && !expectedRecipient) {
1737
+ return errorResult(
1738
+ `Provider "${params.providerNpub}" has no Solana payment address for capability "${params.capability}". Cannot verify payment recipient - refusing to proceed. Ask the provider to publish a capability card with a payment address.`
1739
+ );
1740
+ }
1741
+ const buyerWallet = agent.solanaKeypair?.publicKey;
1742
+ if (buyerWallet && expectedRecipient && buyerWallet === expectedRecipient) {
1743
+ return errorResult(
1744
+ `Cannot buy from yourself - your agent's Solana wallet (${buyerWallet}) matches the provider's payment address. Use a different agent or provider.`
1745
+ );
1746
+ }
1747
+ const submittedAt = Date.now();
1748
+ const jobId = await agent.client.marketplace.submitJobRequest(agent.identity, {
1749
+ input: params.input,
1750
+ capability: params.dTag,
1751
+ providerPubkey: params.providerPubkey,
1752
+ kindOffset: params.kindOffset
1753
+ });
1754
+ let paymentSig;
1755
+ let paidAmountSubunits;
1756
+ let paidAssetKey;
1757
+ let paymentWarnings = [];
1758
+ try {
1759
+ const result = await awaitJobResult(
1760
+ agent,
1761
+ {},
1762
+ ({ resolve, reject }) => {
1763
+ const payHandler = makePaymentFeedbackHandler({
1764
+ ctx,
1765
+ agent,
1766
+ jobId,
1767
+ providerPubkey: params.providerPubkey,
1768
+ expectedRecipient,
1769
+ maxPriceLamports: params.maxPriceLamports,
1770
+ resolveNoWallet: resolve,
1771
+ resolveResult: resolve,
1772
+ rejectPayment: reject,
1773
+ onPaid: (sig, warnings, amount, assetKey5) => {
1774
+ paymentSig = sig;
1775
+ paidAmountSubunits = amount;
1776
+ paidAssetKey = assetKey5;
1777
+ paymentWarnings = warnings;
1778
+ for (const line of warnings) {
1779
+ logger.warn({ event: "session_spend_threshold", agent: agent.name }, line);
1780
+ }
1781
+ }
1782
+ });
1783
+ return {
1784
+ jobEventId: jobId,
1785
+ providerPubkey: params.providerPubkey,
1786
+ customerPublicKey: agent.identity.publicKey,
1787
+ callbacks: {
1788
+ onResult(content) {
1789
+ const kind = isLikelyBase64(content) ? "binary" : "text";
1790
+ const sanitized = sanitizeUntrusted(content, kind);
1791
+ payHandler.onResultReceived(`Job completed.
1792
+
1793
+ ${sanitized.text}`);
1794
+ },
1795
+ onFeedback: payHandler.onFeedback,
1796
+ onError(error) {
1797
+ reject(new Error(`Job error: ${error}`));
1798
+ }
1799
+ },
1800
+ timeoutMs: params.timeoutMs,
1801
+ customerSecretKey: agent.identity.secretKey
1802
+ };
1803
+ },
1804
+ params.timeoutMs + 5e3
1805
+ );
1806
+ await recordJobOutcome(agent, {
1807
+ jobEventId: jobId,
1808
+ capability: params.dTag,
1809
+ providerPubkey: params.providerPubkey,
1810
+ providerName: clipProviderName(provider.name),
1811
+ paidAmountSubunits: paidAmountSubunits?.toString(),
1812
+ assetKey: paidAssetKey,
1813
+ status: "completed",
1814
+ submittedAt,
1815
+ completedAt: Date.now(),
1816
+ resultPreview: result.slice(0, RESULT_PREVIEW_MAX_LEN),
1817
+ paymentSig
1818
+ });
1819
+ const warningBlock = paymentWarnings.length > 0 ? `${paymentWarnings.join("\n")}
1820
+ ` : "";
1821
+ const tip = buildJobCompletionTip(jobId, params.providerNpub);
1822
+ return textResult(`${warningBlock}event_id=${jobId}
1823
+ ${result}${tip}`);
1824
+ } catch (e) {
1825
+ const msg = e instanceof Error ? e.message : String(e);
1826
+ await recordJobOutcome(agent, {
1827
+ jobEventId: jobId,
1828
+ capability: params.dTag,
1829
+ providerPubkey: params.providerPubkey,
1830
+ providerName: clipProviderName(provider.name),
1831
+ paidAmountSubunits: paidAmountSubunits?.toString(),
1832
+ assetKey: paidAssetKey,
1833
+ status: classifyJobFailure(msg),
1834
+ submittedAt,
1835
+ completedAt: Date.now(),
1836
+ paymentSig
1837
+ });
1838
+ const paid = paymentSig ? ` Payment already sent (sig=${paymentSig}) - use get_job_result with event_id="${jobId}" to retrieve once ready.` : "";
1839
+ const warningBlock = paymentWarnings.length > 0 ? `${paymentWarnings.join("\n")}
1840
+ ` : "";
1841
+ return errorResult(`${warningBlock}Job ${jobId} failed: ${msg}.${paid}`);
1842
+ }
1843
+ }
1574
1844
  function awaitJobResult(agent, options, fn, safetyTimeoutMs) {
1575
1845
  return new Promise((resolve, reject) => {
1576
1846
  let closeFn = null;
@@ -1628,9 +1898,10 @@ var customerTools = [
1628
1898
  checkLen("provider_npub", input.provider_npub, MAX_NPUB_LEN);
1629
1899
  const agent = ctx.active();
1630
1900
  const providerPubkey = decodeNpub(input.provider_npub);
1901
+ const dTag = toDTag(input.capability);
1631
1902
  const jobId = await agent.client.marketplace.submitJobRequest(agent.identity, {
1632
1903
  input: input.input,
1633
- capability: input.capability,
1904
+ capability: dTag,
1634
1905
  providerPubkey,
1635
1906
  kindOffset: input.kind_offset
1636
1907
  });
@@ -1639,7 +1910,7 @@ var customerTools = [
1639
1910
  {
1640
1911
  event_id: jobId,
1641
1912
  created_at: Math.floor(Date.now() / 1e3),
1642
- capability: input.capability,
1913
+ capability: dTag,
1643
1914
  provider_npub: input.provider_npub
1644
1915
  },
1645
1916
  null,
@@ -1804,136 +2075,89 @@ ${wrapped}`);
1804
2075
  }),
1805
2076
  defineTool({
1806
2077
  name: "submit_and_pay_job",
1807
- description: "Full customer flow: submit job -> auto-pay -> wait for result. Validates that the payment recipient matches the provider card. On timeout after submission, the job event ID is returned so the caller can follow up with get_job_result. Handles both free and paid providers automatically. If max_price_lamports is not set and provider requests payment, the job is rejected with the price - set max_price_lamports to auto-approve payments up to that limit.",
2078
+ description: "Full customer flow: submit job -> auto-pay -> wait for result. Validates that the payment recipient matches the provider card. On timeout after submission, the job event ID is returned so the caller can follow up with get_job_result. Handles both free and paid providers automatically. If max_price_lamports is not set and provider requests payment, the job is rejected with the price - set max_price_lamports to auto-approve payments up to that limit. COST: input is sent inline in the tool call, so a large input pays output tokens on the calling LLM. For files or git diffs, prefer submit_and_pay_job_from_file or submit_diff_review respectively.",
1808
2079
  schema: SubmitAndPayJobSchema,
1809
2080
  async handler(ctx, input) {
1810
2081
  ctx.toolRateLimiter.check();
1811
2082
  checkLen("input", input.input, MAX_INPUT_LEN);
1812
2083
  checkLen("provider_npub", input.provider_npub, MAX_NPUB_LEN);
1813
- const timeout = Math.min(input.timeout_secs, MAX_TIMEOUT_SECS) * 1e3;
1814
2084
  const agent = ctx.active();
1815
- const providerPubkey = decodeNpub(input.provider_npub);
1816
- const ping = await agent.client.ping.pingAgent(providerPubkey, PRE_PING_TIMEOUT_MS);
1817
- if (!ping.online) {
1818
- return errorResult(
1819
- `Provider ${input.provider_npub} is offline. Run search_agents to find currently-online providers.`
1820
- );
1821
- }
1822
- const providers = await agent.client.discovery.fetchAgents(agent.network);
1823
- const provider = providers.find((a) => a.npub === input.provider_npub);
1824
- if (!provider) {
1825
- return errorResult(
1826
- `Provider ${input.provider_npub} not found on ${agent.network}. Refresh discovery (e.g. search_agents) or verify the npub is correct.`
1827
- );
1828
- }
1829
- const expectedRecipient = providerSolanaAddress(provider, toDTag(input.capability));
1830
- if (agent.solanaKeypair && !expectedRecipient) {
1831
- return errorResult(
1832
- `Provider "${input.provider_npub}" has no Solana payment address for capability "${input.capability}". Cannot verify payment recipient - refusing to proceed. Ask the provider to publish a capability card with a payment address.`
1833
- );
1834
- }
1835
- const buyerWallet = agent.solanaKeypair?.publicKey;
1836
- if (buyerWallet && expectedRecipient && buyerWallet === expectedRecipient) {
1837
- return errorResult(
1838
- `Cannot buy from yourself - your agent's Solana wallet (${buyerWallet}) matches the provider's payment address. Use a different agent or provider.`
1839
- );
1840
- }
1841
- const submittedAt = Date.now();
1842
- const jobId = await agent.client.marketplace.submitJobRequest(agent.identity, {
2085
+ return executeSubmitAndPay(ctx, agent, {
1843
2086
  input: input.input,
2087
+ providerNpub: input.provider_npub,
2088
+ providerPubkey: decodeNpub(input.provider_npub),
1844
2089
  capability: input.capability,
1845
- providerPubkey,
1846
- kindOffset: input.kind_offset
2090
+ // Normalize to canonical d-tag form so the published `t` tag matches what
2091
+ // CLI/App publish on capability cards. Without this, names like
2092
+ // "Opus 4.7" would publish as raw text and the provider's router (which
2093
+ // compares against `toDTag(skill.name)`) would silently drop the job.
2094
+ dTag: toDTag(input.capability),
2095
+ kindOffset: input.kind_offset,
2096
+ timeoutMs: Math.min(input.timeout_secs, MAX_TIMEOUT_SECS) * 1e3,
2097
+ maxPriceLamports: input.max_price_lamports
1847
2098
  });
1848
- let paymentSig;
1849
- let paidAmountSubunits;
1850
- let paidAssetKey;
1851
- let paymentWarnings = [];
2099
+ }
2100
+ }),
2101
+ defineTool({
2102
+ name: "submit_and_pay_job_from_file",
2103
+ description: "Same as submit_and_pay_job, but the job input is read from a file on disk by the MCP server instead of being passed inline by the LLM. Use this when the input is large (logs, generated content, captured output) and the LLM only needs to forward it - the file content never enters the model's output tokens. input_path may be absolute or relative to the MCP server's working directory. Max file size matches the inline limit.",
2104
+ schema: SubmitAndPayJobFromFileSchema,
2105
+ async handler(ctx, input) {
2106
+ ctx.toolRateLimiter.check();
2107
+ checkLen("provider_npub", input.provider_npub, MAX_NPUB_LEN);
2108
+ let payload;
1852
2109
  try {
1853
- const result = await awaitJobResult(
1854
- agent,
1855
- {},
1856
- ({ resolve, reject }) => {
1857
- const payHandler = makePaymentFeedbackHandler({
1858
- ctx,
1859
- agent,
1860
- jobId,
1861
- providerPubkey,
1862
- expectedRecipient,
1863
- maxPriceLamports: input.max_price_lamports,
1864
- resolveNoWallet: resolve,
1865
- resolveResult: resolve,
1866
- rejectPayment: reject,
1867
- onPaid: (sig, warnings, amount, assetKey5) => {
1868
- paymentSig = sig;
1869
- paidAmountSubunits = amount;
1870
- paidAssetKey = assetKey5;
1871
- paymentWarnings = warnings;
1872
- for (const line of warnings) {
1873
- logger.warn({ event: "session_spend_threshold", agent: agent.name }, line);
1874
- }
1875
- }
1876
- });
1877
- return {
1878
- jobEventId: jobId,
1879
- providerPubkey,
1880
- customerPublicKey: agent.identity.publicKey,
1881
- callbacks: {
1882
- onResult(content) {
1883
- const kind = isLikelyBase64(content) ? "binary" : "text";
1884
- const sanitized = sanitizeUntrusted(content, kind);
1885
- payHandler.onResultReceived(`Job completed.
1886
-
1887
- ${sanitized.text}`);
1888
- },
1889
- onFeedback: payHandler.onFeedback,
1890
- onError(error) {
1891
- reject(new Error(`Job error: ${error}`));
1892
- }
1893
- },
1894
- timeoutMs: timeout,
1895
- customerSecretKey: agent.identity.secretKey
1896
- };
1897
- },
1898
- timeout + 5e3
1899
- );
1900
- await recordJobOutcome(agent, {
1901
- jobEventId: jobId,
1902
- capability: input.capability,
1903
- providerPubkey,
1904
- providerName: clipProviderName(provider.name),
1905
- paidAmountSubunits: paidAmountSubunits?.toString(),
1906
- assetKey: paidAssetKey,
1907
- status: "completed",
1908
- submittedAt,
1909
- completedAt: Date.now(),
1910
- resultPreview: result.slice(0, RESULT_PREVIEW_MAX_LEN),
1911
- paymentSig
1912
- });
1913
- const warningBlock = paymentWarnings.length > 0 ? `${paymentWarnings.join("\n")}
1914
- ` : "";
1915
- const tip = buildJobCompletionTip(jobId, input.provider_npub);
1916
- return textResult(`${warningBlock}event_id=${jobId}
1917
- ${result}${tip}`);
2110
+ payload = await readJobInputFile(input.input_path);
1918
2111
  } catch (e) {
1919
- const msg = e instanceof Error ? e.message : String(e);
1920
- await recordJobOutcome(agent, {
1921
- jobEventId: jobId,
1922
- capability: input.capability,
1923
- providerPubkey,
1924
- providerName: clipProviderName(provider.name),
1925
- paidAmountSubunits: paidAmountSubunits?.toString(),
1926
- assetKey: paidAssetKey,
1927
- status: classifyJobFailure(msg),
1928
- submittedAt,
1929
- completedAt: Date.now(),
1930
- paymentSig
1931
- });
1932
- const paid = paymentSig ? ` Payment already sent (sig=${paymentSig}) - use get_job_result with event_id="${jobId}" to retrieve once ready.` : "";
1933
- const warningBlock = paymentWarnings.length > 0 ? `${paymentWarnings.join("\n")}
2112
+ return errorResult(e instanceof Error ? e.message : String(e));
2113
+ }
2114
+ const agent = ctx.active();
2115
+ return executeSubmitAndPay(ctx, agent, {
2116
+ input: payload,
2117
+ providerNpub: input.provider_npub,
2118
+ providerPubkey: decodeNpub(input.provider_npub),
2119
+ capability: input.capability,
2120
+ dTag: toDTag(input.capability),
2121
+ kindOffset: input.kind_offset,
2122
+ timeoutMs: Math.min(input.timeout_secs, MAX_TIMEOUT_SECS) * 1e3,
2123
+ maxPriceLamports: input.max_price_lamports
2124
+ });
2125
+ }
2126
+ }),
2127
+ defineTool({
2128
+ name: "submit_diff_review",
2129
+ description: 'Send a code-review job: the MCP server runs `git diff` inside repo_path and forwards the diff to the chosen provider. The diff content never appears in the LLM\'s output tokens, only the short tool call does. When base is omitted, auto-detects: dirty working tree -> diff against HEAD; clean tree with main/master/origin-HEAD found -> ${detected}...HEAD; otherwise falls back to diff against HEAD. Pass base explicitly (e.g. "main", a tag, or a SHA) to force a `${base}...HEAD` PR-style range. Optional `prompt` is prepended above the diff so reviewers can scope the review. Default capability is "review" - override if the provider advertises a different tag.',
2130
+ schema: SubmitDiffReviewSchema,
2131
+ async handler(ctx, input) {
2132
+ ctx.toolRateLimiter.check();
2133
+ checkLen("provider_npub", input.provider_npub, MAX_NPUB_LEN);
2134
+ let diffResult;
2135
+ try {
2136
+ diffResult = await computeGitDiff(input.repo_path, input.base);
2137
+ } catch (e) {
2138
+ return errorResult(e instanceof Error ? e.message : String(e));
2139
+ }
2140
+ const promptBlock = input.prompt.trim().length > 0 ? `${input.prompt.trim()}
2141
+
1934
2142
  ` : "";
1935
- return errorResult(`${warningBlock}Job ${jobId} failed: ${msg}.${paid}`);
2143
+ const payload = `${promptBlock}--- git diff (${diffResult.describedRange}) ---
2144
+ ${diffResult.diff}`;
2145
+ if (payload.length > MAX_INPUT_LEN) {
2146
+ return errorResult(
2147
+ `Combined prompt + diff is ${payload.length} chars (max ${MAX_INPUT_LEN}). Shorten the prompt or pass a narrower base.`
2148
+ );
1936
2149
  }
2150
+ const agent = ctx.active();
2151
+ return executeSubmitAndPay(ctx, agent, {
2152
+ input: payload,
2153
+ providerNpub: input.provider_npub,
2154
+ providerPubkey: decodeNpub(input.provider_npub),
2155
+ capability: input.capability,
2156
+ dTag: toDTag(input.capability),
2157
+ kindOffset: input.kind_offset,
2158
+ timeoutMs: Math.min(input.timeout_secs, MAX_TIMEOUT_SECS) * 1e3,
2159
+ maxPriceLamports: input.max_price_lamports
2160
+ });
1937
2161
  }
1938
2162
  }),
1939
2163
  defineTool({