@x207/mcp 0.0.0-stage → 0.0.1
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/LICENSE +202 -0
- package/README.md +258 -2
- package/dist/backend.d.ts +25 -0
- package/dist/backend.js +83 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.js +50 -0
- package/dist/config.d.ts +20 -0
- package/dist/config.js +31 -0
- package/dist/guards.d.ts +25 -0
- package/dist/guards.js +67 -0
- package/dist/http.d.ts +19 -0
- package/dist/http.js +94 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +11 -0
- package/dist/net-guard.d.ts +20 -0
- package/dist/net-guard.js +182 -0
- package/dist/server.d.ts +24 -0
- package/dist/server.js +47 -0
- package/dist/tools.d.ts +27 -0
- package/dist/tools.js +147 -0
- package/dist/write-config.d.ts +17 -0
- package/dist/write-config.js +72 -0
- package/dist/write-tools.d.ts +43 -0
- package/dist/write-tools.js +443 -0
- package/package.json +59 -4
package/dist/tools.js
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { BackendError, seg } from "./backend.js";
|
|
3
|
+
/** What a tool says when it cannot do its job: shown to the model as the text of an error result. */
|
|
4
|
+
export class ToolError extends Error {
|
|
5
|
+
}
|
|
6
|
+
const ACCOUNT = z.string().regex(/^[a-z0-9][a-z0-9._-]{1,63}$/, "a NEAR account id, e.g. alice.x207-v013.testnet");
|
|
7
|
+
const HASH = z.string().regex(/^[A-Za-z0-9_-]{8,128}$/, "the hash of the object, as the read API gives it");
|
|
8
|
+
const LIMIT = (max, dflt) => z.number().int().min(1).max(max).default(dflt).describe(`How many items, 1 to ${max} (default ${dflt}).`);
|
|
9
|
+
const EPOCH = z.number().int().min(0).max(4_294_967_295);
|
|
10
|
+
const needBackend = (ctx, what) => {
|
|
11
|
+
if (ctx.backend === null)
|
|
12
|
+
throw new ToolError(`${what} needs the x207 read API: set X207_BACKEND_URL (the server answers from the contract only without it).`);
|
|
13
|
+
return ctx.backend;
|
|
14
|
+
};
|
|
15
|
+
/** Runs the pieces of one answer side by side: the first failure of a piece that is not optional rejects, an optional one becomes a warning. */
|
|
16
|
+
async function gather(required, optional = {}) {
|
|
17
|
+
const keys = [...Object.keys(required), ...Object.keys(optional)];
|
|
18
|
+
const settled = await Promise.allSettled([...Object.values(required), ...Object.values(optional)].map((f) => f()));
|
|
19
|
+
const data = {};
|
|
20
|
+
const warnings = [];
|
|
21
|
+
settled.forEach((r, i) => {
|
|
22
|
+
const key = keys[i];
|
|
23
|
+
if (r.status === "fulfilled")
|
|
24
|
+
data[key] = r.value;
|
|
25
|
+
else if (key in required)
|
|
26
|
+
throw r.reason;
|
|
27
|
+
else
|
|
28
|
+
warnings.push(`${key}: ${r.reason.message}`);
|
|
29
|
+
});
|
|
30
|
+
return { data, warnings };
|
|
31
|
+
}
|
|
32
|
+
export const READ_TOOLS = [
|
|
33
|
+
{
|
|
34
|
+
name: "x207_circle_overview",
|
|
35
|
+
title: "x207 circle overview",
|
|
36
|
+
description: "Overview of one x207 mutual-credit circle: its record and rules from the contract (get_circle, get_params) and its members (read API GET /members, or the contract view list_members when no read API is configured). Credit amounts are strings in the smallest unit (6 decimals).",
|
|
37
|
+
access: "read",
|
|
38
|
+
inputSchema: { members_limit: LIMIT(200, 20), members_status: z.string().min(1).max(32).optional().describe("Only members with this status, e.g. active (read API only).") },
|
|
39
|
+
handler: async (args, ctx) => {
|
|
40
|
+
const { data, warnings } = await gather({ circle: () => ctx.view("get_circle", {}), params: () => ctx.view("get_params", {}) }, {
|
|
41
|
+
members: () => ctx.backend !== null
|
|
42
|
+
? ctx.backend("/members", { limit: args.members_limit, status: args.members_status })
|
|
43
|
+
: ctx.view("list_members", { from_index: 0, limit: args.members_limit }),
|
|
44
|
+
});
|
|
45
|
+
return { contract: ctx.circleContract, circleId: ctx.circleId, ...data, membersSource: ctx.backend !== null ? "read API" : "contract view list_members", warnings };
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
name: "x207_member_inspect",
|
|
50
|
+
title: "Inspect a circle member",
|
|
51
|
+
description: "One member of the circle: status, stake, position and credit limit. Uses the read API (GET /members/{account} and /members/{account}/limits), or the contract views member and limit when no read API is configured.",
|
|
52
|
+
access: "read",
|
|
53
|
+
inputSchema: { account: ACCOUNT },
|
|
54
|
+
handler: async (args, ctx) => {
|
|
55
|
+
const account = args.account;
|
|
56
|
+
if (ctx.backend !== null) {
|
|
57
|
+
const b = ctx.backend;
|
|
58
|
+
const { data, warnings } = await gather({ member: () => b(`/members/${seg(account)}`) }, { limits: () => b(`/members/${seg(account)}/limits`) });
|
|
59
|
+
return { account, ...data, source: "read API", warnings };
|
|
60
|
+
}
|
|
61
|
+
const { data, warnings } = await gather({ member: () => ctx.view("member", { account_id: account }) }, { limit: () => ctx.view("limit", { account_id: account }) });
|
|
62
|
+
if (data.member === null)
|
|
63
|
+
throw new ToolError(`${account} is not a member of ${ctx.circleContract}`);
|
|
64
|
+
return { account, ...data, source: "contract views member and limit", warnings };
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
name: "x207_epoch_report",
|
|
69
|
+
title: "Epoch report (report 207)",
|
|
70
|
+
description: "The state and the result of one clearing epoch: the contract record (get_epoch) and, from the read API, the epoch summary (GET /epochs/{n}), its metrics (cleared ratio, GET /epochs/{n}/metrics) and its settlements (GET /epochs/{n}/settlements). Without epoch, the current epoch.",
|
|
71
|
+
access: "read",
|
|
72
|
+
inputSchema: { epoch: EPOCH.optional().describe("Epoch number; the current epoch when left out."), settlements_limit: LIMIT(200, 20) },
|
|
73
|
+
handler: async (args, ctx) => {
|
|
74
|
+
const record = (await ctx.view("get_epoch", args.epoch === undefined ? {} : { epoch: args.epoch }));
|
|
75
|
+
if (record === null || typeof record.epoch !== "number")
|
|
76
|
+
throw new ToolError(`epoch ${args.epoch ?? "(current)"} is not on the contract`);
|
|
77
|
+
const n = record.epoch;
|
|
78
|
+
if (ctx.backend === null) {
|
|
79
|
+
const { data, warnings } = await gather({}, { positions: () => ctx.view("list_positions", { epoch: n, from_index: 0, limit: args.settlements_limit }) });
|
|
80
|
+
return { epoch: n, contract: record, ...data, source: "contract views get_epoch and list_positions", warnings };
|
|
81
|
+
}
|
|
82
|
+
const b = ctx.backend;
|
|
83
|
+
const { data, warnings } = await gather({}, { summary: () => b(`/epochs/${seg(n)}`), metrics: () => b(`/epochs/${seg(n)}/metrics`), settlements: () => b(`/epochs/${seg(n)}/settlements`, { limit: args.settlements_limit }) });
|
|
84
|
+
return { epoch: n, contract: record, ...data, source: "contract view get_epoch and read API", warnings };
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
name: "x207_simulate_clearing",
|
|
89
|
+
title: "Simulate a clearing proposal",
|
|
90
|
+
description: "Asks the contract (view simulate_clearing) whether a proposed clearing for an epoch is valid and what it would clear. Each edge nets a debt: debtor owes creditor, `cleared` is the amount to cancel (decimal string, smallest unit). Changes nothing.",
|
|
91
|
+
access: "read",
|
|
92
|
+
inputSchema: {
|
|
93
|
+
epoch: EPOCH,
|
|
94
|
+
edges: z.array(z.object({ debtor: ACCOUNT, creditor: ACCOUNT, cleared: z.string().regex(/^\d{1,38}$/, "a decimal integer string") })).min(1).max(300),
|
|
95
|
+
},
|
|
96
|
+
handler: async (args, ctx) => ({ epoch: args.epoch, edgeCount: args.edges.length, result: await ctx.view("simulate_clearing", { epoch: args.epoch, edges: args.edges }) }),
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
name: "x207_search_listings",
|
|
100
|
+
title: "Search marketplace listings",
|
|
101
|
+
description: "Listings of the circle's marketplace (read API GET /listings): what an account offers and what it wants. Filters are optional; `status` defaults to open. Use `cursor` from `nextCursor` for the next page.",
|
|
102
|
+
access: "read",
|
|
103
|
+
inputSchema: {
|
|
104
|
+
status: z.enum(["open", "expired", "cancelled", "filled", "all"]).optional().describe("Default open."),
|
|
105
|
+
account: ACCOUNT.optional().describe("Only listings of this account."),
|
|
106
|
+
offerType: z.string().min(3).max(129).optional().describe("Asset type offered, e.g. service:summary."),
|
|
107
|
+
wantType: z.string().min(3).max(129).optional(),
|
|
108
|
+
offerClass: z.enum(["A", "B", "C", "D", "X"]).optional(),
|
|
109
|
+
wantClass: z.enum(["A", "B", "C", "D", "X"]).optional(),
|
|
110
|
+
limit: LIMIT(200, 20),
|
|
111
|
+
cursor: z.string().min(1).max(512).optional(),
|
|
112
|
+
},
|
|
113
|
+
handler: async (args, ctx) => {
|
|
114
|
+
const b = needBackend(ctx, "x207_search_listings");
|
|
115
|
+
const { status, account, offerType, wantType, offerClass, wantClass, limit, cursor } = args;
|
|
116
|
+
return b("/listings", { status, account, offerType, wantType, offerClass, wantClass, limit, cursor });
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
name: "x207_order_status",
|
|
121
|
+
title: "Order status",
|
|
122
|
+
description: "A service order of the circle's marketplace: parties, budget, milestones and status (read API GET /orders/{hash}). Without a hash, lists orders; filter by `party` and `status`.",
|
|
123
|
+
access: "read",
|
|
124
|
+
inputSchema: {
|
|
125
|
+
hash: HASH.optional().describe("The order hash. Leave out to list orders."),
|
|
126
|
+
party: ACCOUNT.optional().describe("Only orders where this account is buyer or seller (listing only)."),
|
|
127
|
+
status: z.string().min(1).max(32).optional().describe("Only orders with this status (listing only)."),
|
|
128
|
+
limit: LIMIT(200, 20),
|
|
129
|
+
cursor: z.string().min(1).max(512).optional(),
|
|
130
|
+
},
|
|
131
|
+
handler: async (args, ctx) => {
|
|
132
|
+
const b = needBackend(ctx, "x207_order_status");
|
|
133
|
+
if (args.hash !== undefined) {
|
|
134
|
+
try {
|
|
135
|
+
return await b(`/orders/${seg(args.hash)}`);
|
|
136
|
+
}
|
|
137
|
+
catch (error) {
|
|
138
|
+
if (error instanceof BackendError && error.status === 404)
|
|
139
|
+
throw new ToolError(`no order ${args.hash} in circle ${ctx.circleId}`);
|
|
140
|
+
throw error;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return b("/orders", { party: args.party, status: args.status, limit: args.limit, cursor: args.cursor });
|
|
144
|
+
},
|
|
145
|
+
},
|
|
146
|
+
];
|
|
147
|
+
//# sourceMappingURL=tools.js.map
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export interface WriteConfig {
|
|
2
|
+
account: string;
|
|
3
|
+
voucherKey: `ed25519:${string}` | null;
|
|
4
|
+
nearKey: `ed25519:${string}` | null;
|
|
5
|
+
maxSpendSession: bigint;
|
|
6
|
+
maxPricePerCall: bigint;
|
|
7
|
+
allowedCounterparties: readonly string[];
|
|
8
|
+
allowOutflows: boolean;
|
|
9
|
+
allowFullAccessKey: boolean;
|
|
10
|
+
/** `X207_ALLOW_PRIVATE_URLS=1`: x207_pay_request may fetch a URL on this machine or in a private network (a seller you run yourself). Default off (SSRF guard). */
|
|
11
|
+
allowPrivateUrls: boolean;
|
|
12
|
+
}
|
|
13
|
+
/** `null` when the write tools are off. A bad value stops the server with a message that never contains a key. */
|
|
14
|
+
export declare function writeConfigFromEnv(env?: Record<string, string | undefined>): WriteConfig | null;
|
|
15
|
+
/** Takes every secret of the write configuration out of a text, for the error of a tool and for the log. */
|
|
16
|
+
export declare function scrub(text: string, cfg: WriteConfig | null): string;
|
|
17
|
+
//# sourceMappingURL=write-config.d.ts.map
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// Configuration and guards of the WRITE tools of the x207 MCP server (SD-082). Write tools exist only on stdio, only when X207_MCP_WRITE=1, and every key comes from the
|
|
2
|
+
// environment: nothing here prints, logs or returns a key.
|
|
3
|
+
//
|
|
4
|
+
// X207_MCP_WRITE=1 turns the write tools on (default off: the server is read-only)
|
|
5
|
+
// X207_AGENT_ACCOUNT the NEAR account of the agent (a member of the circle)
|
|
6
|
+
// X207_AGENT_VOUCHER_KEY ed25519 voucher secret key of the agent: signs vouchers, listings, offers, acceptances and order notices. It is not a NEAR key.
|
|
7
|
+
// X207_AGENT_NEAR_KEY ed25519 secret key of the NEAR account: signs the transactions of x207_join, x207_settle and x207_claim only
|
|
8
|
+
// X207_MAX_SPEND_SESSION largest total (credit units, 6 decimals) the agent may commit in one session of the server; default 1000000 (1 credit)
|
|
9
|
+
// X207_MAX_PRICE_PER_CALL largest amount of one payment, offer or milestone; default 100000 (0.1 credit)
|
|
10
|
+
// X207_ALLOWED_COUNTERPARTIES comma separated accounts the agent may deal with; empty: any ACTIVE member of the circle
|
|
11
|
+
// X207_ALLOW_OUTFLOWS=1 turns on the tools that move USDT or NEAR out of the agent account into the circle (x207_join, x207_settle); default off
|
|
12
|
+
// X207_ALLOW_PRIVATE_URLS=1 lets x207_pay_request fetch a URL on this machine or in a private network; default off, because the URL comes from a model (SSRF)
|
|
13
|
+
// X207_ALLOW_FULL_ACCESS_KEY=1 lets the NEAR key be a full-access key. Why this exists: a function-call key cannot attach a deposit, and join, stake, settle and claim all
|
|
14
|
+
// attach one (NEAR protocol rule), so the tools that send those transactions need a full-access key of the AGENT account; without this flag they are off.
|
|
15
|
+
import { voucherPublicKey } from "@x207/core";
|
|
16
|
+
import { ConfigError } from "./config.js";
|
|
17
|
+
const ACCOUNT = /^[a-z0-9][a-z0-9._-]{1,63}$/;
|
|
18
|
+
const KEY = /^ed25519:[1-9A-HJ-NP-Za-km-z]{40,100}$/;
|
|
19
|
+
const AMOUNT = /^[1-9]\d{0,37}$/;
|
|
20
|
+
const amount = (env, name, dflt) => {
|
|
21
|
+
const raw = env[name]?.trim() || dflt;
|
|
22
|
+
if (!AMOUNT.test(raw))
|
|
23
|
+
throw new ConfigError(`${name} must be a positive integer (credit units, 6 decimals), got ${JSON.stringify(raw.length > 40 ? "…" : raw)}`);
|
|
24
|
+
return BigInt(raw);
|
|
25
|
+
};
|
|
26
|
+
/** `null` when the write tools are off. A bad value stops the server with a message that never contains a key. */
|
|
27
|
+
export function writeConfigFromEnv(env = process.env) {
|
|
28
|
+
if (env.X207_MCP_WRITE?.trim() !== "1")
|
|
29
|
+
return null;
|
|
30
|
+
const account = env.X207_AGENT_ACCOUNT?.trim() ?? "";
|
|
31
|
+
if (!ACCOUNT.test(account))
|
|
32
|
+
throw new ConfigError("X207_MCP_WRITE=1 needs X207_AGENT_ACCOUNT, the NEAR account of the agent");
|
|
33
|
+
const key = (name) => {
|
|
34
|
+
const v = env[name]?.trim();
|
|
35
|
+
if (v === undefined || v === "")
|
|
36
|
+
return null;
|
|
37
|
+
if (!KEY.test(v))
|
|
38
|
+
throw new ConfigError(`${name} is not an ed25519 secret key (ed25519:<base58>); the value is not shown`);
|
|
39
|
+
return v;
|
|
40
|
+
};
|
|
41
|
+
const voucherKey = key("X207_AGENT_VOUCHER_KEY");
|
|
42
|
+
if (voucherKey !== null) {
|
|
43
|
+
try {
|
|
44
|
+
voucherPublicKey(voucherKey);
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
throw new ConfigError("X207_AGENT_VOUCHER_KEY is not a usable ed25519 secret key; the value is not shown");
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
const allowed = (env.X207_ALLOWED_COUNTERPARTIES ?? "").split(",").map((s) => s.trim()).filter((s) => s !== "");
|
|
51
|
+
for (const a of allowed)
|
|
52
|
+
if (!ACCOUNT.test(a))
|
|
53
|
+
throw new ConfigError(`X207_ALLOWED_COUNTERPARTIES: ${JSON.stringify(a)} is not an account id`);
|
|
54
|
+
return {
|
|
55
|
+
account, voucherKey, nearKey: key("X207_AGENT_NEAR_KEY"),
|
|
56
|
+
maxSpendSession: amount(env, "X207_MAX_SPEND_SESSION", "1000000"),
|
|
57
|
+
maxPricePerCall: amount(env, "X207_MAX_PRICE_PER_CALL", "100000"),
|
|
58
|
+
allowedCounterparties: allowed,
|
|
59
|
+
allowOutflows: env.X207_ALLOW_OUTFLOWS?.trim() === "1",
|
|
60
|
+
allowFullAccessKey: env.X207_ALLOW_FULL_ACCESS_KEY?.trim() === "1",
|
|
61
|
+
allowPrivateUrls: env.X207_ALLOW_PRIVATE_URLS?.trim() === "1",
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
/** Takes every secret of the write configuration out of a text, for the error of a tool and for the log. */
|
|
65
|
+
export function scrub(text, cfg) {
|
|
66
|
+
let out = text;
|
|
67
|
+
for (const secret of [cfg?.voucherKey, cfg?.nearKey])
|
|
68
|
+
if (secret)
|
|
69
|
+
out = out.split(secret).join("[key]");
|
|
70
|
+
return out.replace(/ed25519:[1-9A-HJ-NP-Za-km-z]{60,100}/g, "[key]");
|
|
71
|
+
}
|
|
72
|
+
//# sourceMappingURL=write-config.js.map
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { type CircleViewFn, type ContractCall } from "@x207/core";
|
|
2
|
+
import type { BackendPost } from "./backend.js";
|
|
3
|
+
import type { McpConfig } from "./config.js";
|
|
4
|
+
import { Guards, type NearKeyKind } from "./guards.js";
|
|
5
|
+
import { type HostResolver } from "./net-guard.js";
|
|
6
|
+
import { type ToolDef } from "./tools.js";
|
|
7
|
+
import type { WriteConfig } from "./write-config.js";
|
|
8
|
+
export interface WriteRuntime {
|
|
9
|
+
cfg: WriteConfig;
|
|
10
|
+
guards: Guards;
|
|
11
|
+
/** Signs and sends a NEAR transaction with the NEAR key; waits for finality. */
|
|
12
|
+
send: (call: ContractCall) => Promise<unknown>;
|
|
13
|
+
/** What kind of access key the NEAR key is on the agent account. */
|
|
14
|
+
keyKind: () => Promise<NearKeyKind>;
|
|
15
|
+
/** The fetch that pays an x402 402 with an x207 voucher; created on first use and kept for the session, so the sequence numbers of the vouchers go on. */
|
|
16
|
+
payFetch: () => typeof fetch;
|
|
17
|
+
post: BackendPost | null;
|
|
18
|
+
now: () => number;
|
|
19
|
+
/** Resolves a host name for the SSRF guard of x207_pay_request. */
|
|
20
|
+
resolve: HostResolver;
|
|
21
|
+
}
|
|
22
|
+
/** The most that x207_pay_request reads of one answer. */
|
|
23
|
+
export declare const MAX_PAID_BODY_BYTES = 1048576;
|
|
24
|
+
/**
|
|
25
|
+
* After a commitment failed: the amount goes back to the budget only when it is certain that nothing was committed (a refusal of ours, a 4xx answer of the book, a contract that said no). A timeout, a 5xx
|
|
26
|
+
* or an unknown error may have committed it, so the amount stays counted and the model is told that the outcome is unknown (cross review UM-112, F3).
|
|
27
|
+
*/
|
|
28
|
+
export declare function keepIfUnknown(rt: {
|
|
29
|
+
guards: Guards;
|
|
30
|
+
}, amount: bigint, error: unknown): unknown;
|
|
31
|
+
export declare const WRITE_TOOLS: ToolDef[];
|
|
32
|
+
/** Which write tools this runtime may even list: a tool whose key or switch is missing is not offered (the guard would refuse it anyway). */
|
|
33
|
+
export declare function availableWriteTools(rt: WriteRuntime, hasBackend: boolean): ToolDef[];
|
|
34
|
+
/** Builds the runtime of the write tools from the configuration. The NEAR sender and the payment client are created on first use. */
|
|
35
|
+
export declare function createWriteRuntime(config: McpConfig, cfg: WriteConfig, view: CircleViewFn, deps?: {
|
|
36
|
+
post: BackendPost | null;
|
|
37
|
+
send?: (call: ContractCall) => Promise<unknown>;
|
|
38
|
+
now?: () => number;
|
|
39
|
+
resolve?: HostResolver;
|
|
40
|
+
}): WriteRuntime;
|
|
41
|
+
/** `view_access_key` of the NEAR key on the agent account. `unknown` when the key is not on the account or the RPC cannot say. */
|
|
42
|
+
export declare function accessKeyKind(rpcUrls: readonly string[], account: string, publicKey: string, fetchFn: typeof fetch): Promise<NearKeyKind>;
|
|
43
|
+
//# sourceMappingURL=write-tools.d.ts.map
|