@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 +26 -1
- package/dist/index.js +345 -121
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -5,7 +5,6 @@
|
|
|
5
5
|
[](https://github.com/elisymlabs/elisym/pkgs/container/mcp)
|
|
6
6
|
[](https://registry.modelcontextprotocol.io/v0/servers?search=elisym)
|
|
7
7
|
[](../../LICENSE)
|
|
8
|
-
[](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:
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
1846
|
-
|
|
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
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
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
|
-
|
|
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({
|