@agtnames/mcp 1.1.0 → 1.2.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 +15 -1
- package/dist/config.js +4 -1
- package/dist/server.js +17 -2
- package/dist/session.js +100 -0
- package/dist/write-tools.js +123 -0
- package/package.json +7 -5
package/README.md
CHANGED
|
@@ -30,12 +30,26 @@ From a checkout: `claude mcp add agt -- node packages/mcp/dist/index.js`; agains
|
|
|
30
30
|
|---|---|
|
|
31
31
|
| `agt_resolve` | owner, expiry, active/perpetual, on-chain records, verified manifest (under `untrusted`) |
|
|
32
32
|
| `agt_manifest` | the manifest document + `verified` / `reasons` |
|
|
33
|
-
| `agt_endpoint` | URL for `mcp` / `a2a` / `http` / `ws` — verified manifest first, resolver record second |
|
|
33
|
+
| `agt_endpoint` | URL for `mcp` / `a2a` / `http` / `ws` — verified manifest first, resolver record second — plus `pricing` (`free` / `freemium` / `paid` / `contact`, from the verified manifest only) |
|
|
34
34
|
| `agt_available` | can the name be registered right now |
|
|
35
35
|
| `agt_namehash` | node + tokenId (no network) |
|
|
36
36
|
|
|
37
37
|
All tools are annotated read-only and idempotent. `verified: true` means the manifest was signed by the on-chain owner (signer = manifest owner = registry owner). Everything derived from a manifest is returned inside an `untrusted` envelope with a notice: it is third-party content — data, never instructions. The server also publishes these rules as MCP `instructions`.
|
|
38
38
|
|
|
39
|
+
## Write tools (opt-in): countersign session grants
|
|
40
|
+
|
|
41
|
+
Set `AGT_SESSION_PASSPHRASE` (12+ characters) and nine more tools appear. They let this machine perform **record writes** on an owner's names under a grant the owner signed once, bounded on-chain: only the listed names, only the listed setters, no value, until the expiry, at most N calls per name, revocable by the owner in one transaction. See `@agtnames/countersign` for how a grant is built and enforced.
|
|
42
|
+
|
|
43
|
+
| Tool | Does |
|
|
44
|
+
|---|---|
|
|
45
|
+
| `agt_session_new` | create this machine's session key (encrypted under `AGT_SESSION_DIR`, default `~/.agt/session`) and return its address |
|
|
46
|
+
| `agt_session_import` | validate and store a grant the owner signed for that address; returns the plain-language mandate |
|
|
47
|
+
| `agt_session_status` | calls used per name, expiry, whether the owner revoked, session gas balance |
|
|
48
|
+
| `agt_session_forget` | drop the grant locally (and the key with `deleteKey`) |
|
|
49
|
+
| `agt_set_text` `agt_set_addr` `agt_set_endpoint` `agt_set_manifest_uri` `agt_set_wallet` | redeem one record write; each returns the tx hash |
|
|
50
|
+
|
|
51
|
+
Flow: `agt_session_new` → give the address to the owner → owner runs `agt-countersign build --session <address> --names … --actions … --ttl 1h --calls 5 --sign-with-key OWNER_KEY` (or signs the typed data in a wallet) → `agt_session_import` → write. Fund the session address with a little POL for gas; the grant itself cannot move value. Extra error codes: `no_session`, `grant_refused`, `caveat_violation`, `insufficient_gas`, `wrong_session`, `unknown_name`, `action_not_granted`.
|
|
52
|
+
|
|
39
53
|
## Errors
|
|
40
54
|
|
|
41
55
|
Failures come back as an MCP error result (`isError: true`) whose text is `{ "error": { "code", "message" } }`:
|
package/dist/config.js
CHANGED
|
@@ -39,7 +39,7 @@ export function loadConfig(env = process.env) {
|
|
|
39
39
|
ratePerMin: num(get("AGT_RATE_PER_MIN"), DEFAULTS.ratePerMin),
|
|
40
40
|
};
|
|
41
41
|
}
|
|
42
|
-
export const HELP = `@agtnames/mcp ${VERSION} — MCP server for .agt agent names (stdio, read-only)
|
|
42
|
+
export const HELP = `@agtnames/mcp ${VERSION} — MCP server for .agt agent names (stdio, read-only by default)
|
|
43
43
|
|
|
44
44
|
Usage: agt-mcp [--version] [--help]
|
|
45
45
|
Runs an MCP server over stdio. Any MCP-compatible client can launch it, e.g.
|
|
@@ -63,4 +63,7 @@ Environment (all optional; Polygon mainnet works with none)
|
|
|
63
63
|
AGT_TIMEOUT_MS per-request timeout (default ${DEFAULTS.timeoutMs})
|
|
64
64
|
AGT_MAX_MANIFEST max manifest bytes (default ${DEFAULTS.maxManifestBytes})
|
|
65
65
|
AGT_RATE_PER_MIN tool calls per minute (default ${DEFAULTS.ratePerMin})
|
|
66
|
+
AGT_SESSION_PASSPHRASE enables the countersign write tools (agt_session_*, agt_set_*): the local session key
|
|
67
|
+
is stored encrypted under AGT_SESSION_DIR (default ~/.agt/session) and redeems an
|
|
68
|
+
owner-signed grant; writes are bounded by the grant's on-chain caveats
|
|
66
69
|
`;
|
package/dist/server.js
CHANGED
|
@@ -17,6 +17,8 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
|
17
17
|
import { z } from "zod";
|
|
18
18
|
import { AgtResolver, namehash, normalizeName, tokenIdOf } from "@agtnames/resolver";
|
|
19
19
|
import { VERSION } from "./config.js";
|
|
20
|
+
import { sessionConfig } from "./session.js";
|
|
21
|
+
import { registerWriteTools } from "./write-tools.js";
|
|
20
22
|
export class McpToolError extends Error {
|
|
21
23
|
code;
|
|
22
24
|
constructor(code, message) {
|
|
@@ -77,6 +79,16 @@ export const LIMITS = { str: 512, url: 2048, list: 50, depth: 4, key: 64 };
|
|
|
77
79
|
// C0 controls + DEL, built from code points (no escape literals in source)
|
|
78
80
|
const CONTROL_CHARS = new RegExp("[" + String.fromCharCode(0) + "-" + String.fromCharCode(31) + String.fromCharCode(127) + "]", "g");
|
|
79
81
|
export const clean = (s, max) => s.replace(CONTROL_CHARS, "").slice(0, max);
|
|
82
|
+
/**
|
|
83
|
+
* `pricing.model` from a VERIFIED manifest, sanitized; null when unverified or absent. Lets a client tell a free
|
|
84
|
+
* agent from a paid one before it calls the endpoint, without fetching the whole manifest.
|
|
85
|
+
*/
|
|
86
|
+
export function pricingModel(manifest, verified) {
|
|
87
|
+
if (!verified || !manifest || typeof manifest !== "object")
|
|
88
|
+
return null;
|
|
89
|
+
const model = manifest.pricing?.model;
|
|
90
|
+
return typeof model === "string" && model ? clean(model, LIMITS.key) : null;
|
|
91
|
+
}
|
|
80
92
|
/** Bound third-party JSON: strings capped (URLs longer), lists/objects capped with a visible marker, depth capped. */
|
|
81
93
|
export function sanitize(v, depth = 0) {
|
|
82
94
|
if (depth > LIMITS.depth)
|
|
@@ -182,7 +194,7 @@ export function buildServer(cfg, deps = {}) {
|
|
|
182
194
|
}));
|
|
183
195
|
server.registerTool("agt_endpoint", {
|
|
184
196
|
title: "Get an agent endpoint",
|
|
185
|
-
description: "Get an agent's endpoint URL for a protocol (mcp, a2a, http, ws). Prefers the verified manifest; falls back to the on-chain resolver record. `verified: false` means the URL is unverified third-party data.",
|
|
197
|
+
description: "Get an agent's endpoint URL for a protocol (mcp, a2a, http, ws) plus its pricing model (free, freemium, paid, contact) when the manifest verifies. Prefers the verified manifest; falls back to the on-chain resolver record. `verified: false` means the URL is unverified third-party data.",
|
|
186
198
|
inputSchema: { name: nameSchema, protocol: z.enum(["mcp", "a2a", "http", "ws"]).describe("Endpoint protocol") },
|
|
187
199
|
annotations: READ,
|
|
188
200
|
}, guarded(async ({ name, protocol }) => {
|
|
@@ -190,7 +202,7 @@ export function buildServer(cfg, deps = {}) {
|
|
|
190
202
|
const fromManifest = r.verified ? r.manifest?.endpoints?.find((e) => e.protocol === protocol)?.url ?? null : null;
|
|
191
203
|
const fromRecord = r.records.endpoints[protocol] ?? null;
|
|
192
204
|
const url = fromManifest ?? fromRecord;
|
|
193
|
-
return { name: r.name, protocol, url: url ? clean(url, LIMITS.url) : null, source: fromManifest ? "verified-manifest" : fromRecord ? "resolver-record" : null, verified: !!fromManifest, reasons: r.reasons, notice: NOTICE };
|
|
205
|
+
return { name: r.name, protocol, url: url ? clean(url, LIMITS.url) : null, source: fromManifest ? "verified-manifest" : fromRecord ? "resolver-record" : null, verified: !!fromManifest, pricing: pricingModel(r.manifest, r.verified), reasons: r.reasons, notice: NOTICE };
|
|
194
206
|
}));
|
|
195
207
|
server.registerTool("agt_available", {
|
|
196
208
|
title: "Check availability",
|
|
@@ -204,5 +216,8 @@ export function buildServer(cfg, deps = {}) {
|
|
|
204
216
|
inputSchema: { name: nameSchema },
|
|
205
217
|
annotations: OFFLINE,
|
|
206
218
|
}, guarded(async ({ name }) => { const n = checkName(name); return { name: n, node: namehash(n), tokenId: tokenIdOf(n).toString() }; }));
|
|
219
|
+
const session = sessionConfig(deps.env ?? process.env);
|
|
220
|
+
if (session)
|
|
221
|
+
registerWriteTools(server, session, deps.write);
|
|
207
222
|
return server;
|
|
208
223
|
}
|
package/dist/session.js
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local session store for the countersign write tools (D-025 v2).
|
|
3
|
+
*
|
|
4
|
+
* The session key never leaves this machine. It is generated here, stored encrypted (scrypt + AES-256-GCM) under
|
|
5
|
+
* AGT_SESSION_DIR, and used only to redeem a grant the owner signed. The grant itself is not secret and is stored
|
|
6
|
+
* alongside as plain JSON. Caveats on-chain, not this encryption, bound what a compromised running agent can do.
|
|
7
|
+
*/
|
|
8
|
+
import { randomBytes, scryptSync, createCipheriv, createDecipheriv } from "node:crypto";
|
|
9
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, rmSync } from "node:fs";
|
|
10
|
+
import { homedir } from "node:os";
|
|
11
|
+
import path from "node:path";
|
|
12
|
+
import { generatePrivateKey, privateKeyToAccount } from "viem/accounts";
|
|
13
|
+
import { decodeGrant, describeGrant } from "@agtnames/countersign";
|
|
14
|
+
export function sessionConfig(env = process.env) {
|
|
15
|
+
const passphrase = env.AGT_SESSION_PASSPHRASE?.trim();
|
|
16
|
+
if (!passphrase)
|
|
17
|
+
return null;
|
|
18
|
+
if (passphrase.length < 12)
|
|
19
|
+
throw new Error("AGT_SESSION_PASSPHRASE must be at least 12 characters");
|
|
20
|
+
const dir = env.AGT_SESSION_DIR?.trim() || path.join(homedir(), ".agt", "session");
|
|
21
|
+
return { dir, passphrase };
|
|
22
|
+
}
|
|
23
|
+
const KDF = { N: 2 ** 15, r: 8, p: 1 };
|
|
24
|
+
const keyFile = (c) => path.join(c.dir, "session.json");
|
|
25
|
+
const grantFile = (c) => path.join(c.dir, "grant.json");
|
|
26
|
+
function encrypt(pk, c) {
|
|
27
|
+
const salt = randomBytes(16);
|
|
28
|
+
const key = scryptSync(c.passphrase.normalize("NFKC"), salt, 32, { ...KDF, maxmem: 128 * 1024 * 1024 });
|
|
29
|
+
const iv = randomBytes(12);
|
|
30
|
+
const cipher = createCipheriv("aes-256-gcm", key, iv);
|
|
31
|
+
const ct = Buffer.concat([cipher.update(Buffer.from(pk.slice(2), "hex")), cipher.final()]);
|
|
32
|
+
return { v: 1, address: privateKeyToAccount(pk).address, kdf: { ...KDF, salt: salt.toString("base64") }, iv: iv.toString("base64"), ct: ct.toString("base64"), tag: cipher.getAuthTag().toString("base64"), createdAt: new Date().toISOString() };
|
|
33
|
+
}
|
|
34
|
+
function decrypt(e, c) {
|
|
35
|
+
const key = scryptSync(c.passphrase.normalize("NFKC"), Buffer.from(e.kdf.salt, "base64"), 32, { N: e.kdf.N, r: e.kdf.r, p: e.kdf.p, maxmem: 128 * 1024 * 1024 });
|
|
36
|
+
const d = createDecipheriv("aes-256-gcm", key, Buffer.from(e.iv, "base64"));
|
|
37
|
+
d.setAuthTag(Buffer.from(e.tag, "base64"));
|
|
38
|
+
const pk = Buffer.concat([d.update(Buffer.from(e.ct, "base64")), d.final()]);
|
|
39
|
+
return ("0x" + pk.toString("hex"));
|
|
40
|
+
}
|
|
41
|
+
export class SessionStore {
|
|
42
|
+
c;
|
|
43
|
+
constructor(c) {
|
|
44
|
+
this.c = c;
|
|
45
|
+
}
|
|
46
|
+
/** Session address if a key exists, else null. Does not need the passphrase to be right. */
|
|
47
|
+
address() {
|
|
48
|
+
if (!existsSync(keyFile(this.c)))
|
|
49
|
+
return null;
|
|
50
|
+
return JSON.parse(readFileSync(keyFile(this.c), "utf8")).address;
|
|
51
|
+
}
|
|
52
|
+
/** Create the session key if none exists. Returns the address (never the key). */
|
|
53
|
+
ensureKey() {
|
|
54
|
+
const existing = this.address();
|
|
55
|
+
if (existing)
|
|
56
|
+
return { address: existing, created: false };
|
|
57
|
+
mkdirSync(this.c.dir, { recursive: true });
|
|
58
|
+
const pk = generatePrivateKey();
|
|
59
|
+
writeFileSync(keyFile(this.c), JSON.stringify(encrypt(pk, this.c), null, 2), { mode: 0o600 });
|
|
60
|
+
return { address: privateKeyToAccount(pk).address, created: true };
|
|
61
|
+
}
|
|
62
|
+
/** Decrypt the session key for one redemption. Throws on a wrong passphrase. */
|
|
63
|
+
privateKey() {
|
|
64
|
+
if (!existsSync(keyFile(this.c)))
|
|
65
|
+
throw new Error("no session key; call agt_session_new first");
|
|
66
|
+
try {
|
|
67
|
+
return decrypt(JSON.parse(readFileSync(keyFile(this.c), "utf8")), this.c);
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
throw new Error("AGT_SESSION_PASSPHRASE does not unlock the stored session key");
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
/** Validate a grant blob against this session and store it. */
|
|
74
|
+
async importGrant(blob, now = Math.floor(Date.now() / 1000)) {
|
|
75
|
+
const grant = decodeGrant(blob);
|
|
76
|
+
const address = this.address();
|
|
77
|
+
if (!address)
|
|
78
|
+
throw new Error("no session key; call agt_session_new first, then have the owner sign a grant for that address");
|
|
79
|
+
if (grant.delegate.toLowerCase() !== address.toLowerCase())
|
|
80
|
+
throw new Error(`grant delegate ${grant.delegate} is not this session (${address}); the owner must sign a grant for this session's address`);
|
|
81
|
+
const d = await describeGrant(grant, { now });
|
|
82
|
+
if (!d.ok)
|
|
83
|
+
throw new Error(`grant refused: ${d.problems.join("; ")}`);
|
|
84
|
+
mkdirSync(this.c.dir, { recursive: true });
|
|
85
|
+
writeFileSync(grantFile(this.c), JSON.stringify(grant), { mode: 0o600 });
|
|
86
|
+
return { grant, description: d };
|
|
87
|
+
}
|
|
88
|
+
grant() {
|
|
89
|
+
if (!existsSync(grantFile(this.c)))
|
|
90
|
+
return null;
|
|
91
|
+
return decodeGrant(readFileSync(grantFile(this.c), "utf8"));
|
|
92
|
+
}
|
|
93
|
+
/** Forget the grant (and optionally the key). Local only: on-chain revocation is the owner's nonce bump. */
|
|
94
|
+
forget(opts = {}) {
|
|
95
|
+
if (existsSync(grantFile(this.c)))
|
|
96
|
+
rmSync(grantFile(this.c));
|
|
97
|
+
if (opts.key && existsSync(keyFile(this.c)))
|
|
98
|
+
rmSync(keyFile(this.c));
|
|
99
|
+
}
|
|
100
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { ACTIONS, describeGrant, grantStatus, redeem, RedeemError, GrantError } from "@agtnames/countersign";
|
|
3
|
+
import { SessionStore } from "./session.js";
|
|
4
|
+
import { bounded, checkName, McpToolError } from "./server.js";
|
|
5
|
+
export const WRITE_TOOL_NAMES = [
|
|
6
|
+
"agt_session_new", "agt_session_import", "agt_session_status", "agt_session_forget",
|
|
7
|
+
"agt_set_text", "agt_set_addr", "agt_set_endpoint", "agt_set_manifest_uri", "agt_set_wallet",
|
|
8
|
+
];
|
|
9
|
+
const WRITE = { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true };
|
|
10
|
+
const LOCAL = { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false };
|
|
11
|
+
const LOCAL_READ = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true };
|
|
12
|
+
export const WRITE_NOTICE = "Writes are bounded by the grant's on-chain caveats: only the listed names, only the listed record setters, no value, until the expiry, at most maxCalls per name. The owner revokes everything by bumping their NonceEnforcer nonce.";
|
|
13
|
+
const ok = (v) => ({ content: [{ type: "text", text: bounded(v) }] });
|
|
14
|
+
const err = (code, message) => ({ content: [{ type: "text", text: JSON.stringify({ error: { code, message } }) }], isError: true });
|
|
15
|
+
function classify(e) {
|
|
16
|
+
if (e instanceof RedeemError)
|
|
17
|
+
return err(e.code === "rpc_error" ? "rpc_error" : e.code, e.message);
|
|
18
|
+
if (e instanceof GrantError)
|
|
19
|
+
return err("grant_refused", `${e.code}: ${e.message}`);
|
|
20
|
+
if (e instanceof McpToolError)
|
|
21
|
+
return err(e.code, e.message);
|
|
22
|
+
const m = e instanceof Error ? e.message : String(e);
|
|
23
|
+
if (/no session key/i.test(m))
|
|
24
|
+
return err("no_session", m);
|
|
25
|
+
if (/grant refused|not this session|delegate/i.test(m))
|
|
26
|
+
return err("grant_refused", m);
|
|
27
|
+
return err("internal", m.slice(0, 300));
|
|
28
|
+
}
|
|
29
|
+
export function registerWriteTools(server, cfg, deps = {}) {
|
|
30
|
+
const store = new SessionStore(cfg);
|
|
31
|
+
const doRedeem = deps.redeem ?? redeem;
|
|
32
|
+
const doStatus = deps.status ?? grantStatus;
|
|
33
|
+
const now = deps.now ?? (() => Math.floor(Date.now() / 1000));
|
|
34
|
+
const nameSchema = z.string().min(1).max(70).describe("A .agt name covered by the imported grant");
|
|
35
|
+
const loadGrant = () => {
|
|
36
|
+
const g = store.grant();
|
|
37
|
+
if (!g)
|
|
38
|
+
throw new McpToolError("misconfigured", "no grant imported; call agt_session_import with a grant the owner signed for this session");
|
|
39
|
+
if (now() >= g.notAfter)
|
|
40
|
+
throw new RedeemError("caveat_violation", `grant expired at ${new Date(g.notAfter * 1000).toISOString()}; ask the owner for a new one`);
|
|
41
|
+
return g;
|
|
42
|
+
};
|
|
43
|
+
const write = async (name, args) => {
|
|
44
|
+
const g = loadGrant();
|
|
45
|
+
const n = checkName(name);
|
|
46
|
+
const r = await doRedeem(g, n, args, { session: store.privateKey(), rpcUrl: deps.rpcUrl });
|
|
47
|
+
return { name: n, action: ACTIONS[args.action].functionName, txHash: r.hash, blockNumber: r.blockNumber.toString(), chainId: g.chainId, notice: WRITE_NOTICE };
|
|
48
|
+
};
|
|
49
|
+
const guarded = (fn) => async (a) => { try {
|
|
50
|
+
return ok(await fn(a));
|
|
51
|
+
}
|
|
52
|
+
catch (e) {
|
|
53
|
+
return classify(e);
|
|
54
|
+
} };
|
|
55
|
+
server.registerTool("agt_session_new", {
|
|
56
|
+
title: "Create the local session key",
|
|
57
|
+
description: "Create (or show) this machine's countersign session key and return its address. Give the address to the name owner: they sign a grant for it (names, record setters, expiry, call count) and you import that grant with agt_session_import. The private key never leaves this machine.",
|
|
58
|
+
inputSchema: {},
|
|
59
|
+
annotations: LOCAL,
|
|
60
|
+
}, guarded(async () => {
|
|
61
|
+
const { address, created } = store.ensureKey();
|
|
62
|
+
return { sessionAddress: address, created, next: `Owner: build a grant for delegate ${address} (agt-countersign build --session ${address} …), sign it, and paste the JSON into agt_session_import. Fund ${address} with a little POL for gas.` };
|
|
63
|
+
}));
|
|
64
|
+
server.registerTool("agt_session_import", {
|
|
65
|
+
title: "Import a signed grant",
|
|
66
|
+
description: "Validate and store a grant the owner signed for this session. Refuses a grant for another session, an expired grant, or one whose caveats do not match its summary. Returns the plain-language mandate the chain will enforce.",
|
|
67
|
+
inputSchema: { grant: z.string().min(2).max(64 * 1024).describe("Grant JSON as produced by agt-countersign build (or the owner's wallet flow)") },
|
|
68
|
+
annotations: LOCAL,
|
|
69
|
+
}, guarded(async ({ grant }) => {
|
|
70
|
+
const { grant: g, description } = await store.importGrant(grant, now());
|
|
71
|
+
return { imported: true, grantHash: g.hash, names: g.names.map((n) => n.name), actions: g.actions, notAfter: new Date(g.notAfter * 1000).toISOString(), maxCallsPerName: g.maxCalls, mandate: description.summary, notice: WRITE_NOTICE };
|
|
72
|
+
}));
|
|
73
|
+
server.registerTool("agt_session_status", {
|
|
74
|
+
title: "Session and grant status",
|
|
75
|
+
description: "Show the session address, whether a grant is imported, and (from the chain) calls used per name, expiry, whether the owner has revoked, and the session's gas balance.",
|
|
76
|
+
inputSchema: {},
|
|
77
|
+
annotations: LOCAL_READ,
|
|
78
|
+
}, guarded(async () => {
|
|
79
|
+
const address = store.address();
|
|
80
|
+
const g = store.grant();
|
|
81
|
+
if (!g)
|
|
82
|
+
return { sessionAddress: address, grant: null, hint: address ? "no grant imported yet" : "no session key yet; call agt_session_new" };
|
|
83
|
+
const d = await describeGrant(g, { now: now(), verifySignatures: false });
|
|
84
|
+
const s = await doStatus(g, { rpcUrl: deps.rpcUrl, now: now() });
|
|
85
|
+
return { sessionAddress: address, grantHash: g.hash, mandate: d.summary, onchain: s };
|
|
86
|
+
}));
|
|
87
|
+
server.registerTool("agt_session_forget", {
|
|
88
|
+
title: "Forget the local grant",
|
|
89
|
+
description: "Delete the imported grant from this machine (and the session key too if deleteKey is true). This is local housekeeping only: to revoke on-chain, the owner bumps their NonceEnforcer nonce or disables the delegation.",
|
|
90
|
+
inputSchema: { deleteKey: z.boolean().default(false).describe("Also delete the session private key") },
|
|
91
|
+
annotations: LOCAL,
|
|
92
|
+
}, guarded(async ({ deleteKey }) => { store.forget({ key: deleteKey }); return { forgotten: true, keyDeleted: deleteKey, reminder: "On-chain revocation is the owner's: bump the NonceEnforcer nonce (revoke all) or DelegationManager.disableDelegation (one grant)." }; }));
|
|
93
|
+
server.registerTool("agt_set_text", {
|
|
94
|
+
title: "Set a text record (under the grant)",
|
|
95
|
+
description: `Write a text record (url, description, avatar, com.twitter, …) on a granted name. ${ACTIONS.text.describe}. Redeemed through the owner's delegation; reverts if the name, key type or call budget is outside the grant.`,
|
|
96
|
+
inputSchema: { name: nameSchema, key: z.string().min(1).max(64), value: z.string().max(2048) },
|
|
97
|
+
annotations: WRITE,
|
|
98
|
+
}, guarded(({ name, key, value }) => write(name, { action: "text", key, value })));
|
|
99
|
+
server.registerTool("agt_set_addr", {
|
|
100
|
+
title: "Set the address record (under the grant)",
|
|
101
|
+
description: `Write the primary address record on a granted name. ${ACTIONS.addr.describe}.`,
|
|
102
|
+
inputSchema: { name: nameSchema, address: z.string().regex(/^0x[0-9a-fA-F]{40}$/) },
|
|
103
|
+
annotations: WRITE,
|
|
104
|
+
}, guarded(({ name, address }) => write(name, { action: "addr", address: address })));
|
|
105
|
+
server.registerTool("agt_set_endpoint", {
|
|
106
|
+
title: "Set an agent endpoint (under the grant)",
|
|
107
|
+
description: `Write the endpoint URL for a protocol on a granted name. ${ACTIONS.endpoint.describe}.`,
|
|
108
|
+
inputSchema: { name: nameSchema, protocol: z.enum(["mcp", "a2a", "http", "ws"]), url: z.string().url().max(2048) },
|
|
109
|
+
annotations: WRITE,
|
|
110
|
+
}, guarded(({ name, protocol, url }) => write(name, { action: "endpoint", protocol, url })));
|
|
111
|
+
server.registerTool("agt_set_manifest_uri", {
|
|
112
|
+
title: "Point the name at a manifest URI (under the grant)",
|
|
113
|
+
description: `Write the on-chain manifest pointer on a granted name. ${ACTIONS.manifest.describe}. The manifest document itself must still be signed by the owner's key; this tool only moves the pointer.`,
|
|
114
|
+
inputSchema: { name: nameSchema, uri: z.string().min(1).max(2048) },
|
|
115
|
+
annotations: WRITE,
|
|
116
|
+
}, guarded(({ name, uri }) => write(name, { action: "manifest", uri })));
|
|
117
|
+
server.registerTool("agt_set_wallet", {
|
|
118
|
+
title: "Set the agent wallet record (under the grant)",
|
|
119
|
+
description: `Write the agent wallet record on a granted name. ${ACTIONS.wallet.describe}.`,
|
|
120
|
+
inputSchema: { name: nameSchema, wallet: z.string().regex(/^0x[0-9a-fA-F]{40}$/) },
|
|
121
|
+
annotations: WRITE,
|
|
122
|
+
}, guarded(({ name, wallet }) => write(name, { action: "wallet", wallet: wallet })));
|
|
123
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agtnames/mcp",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "MCP server for .agt agent names: resolve, verify and discover agents by name against AGT Registry v2. Works with any MCP-compatible client (Claude Code, Cursor, custom agents).",
|
|
3
|
+
"version": "1.2.0",
|
|
4
|
+
"description": "MCP server for .agt agent names: resolve, verify and discover agents by name against AGT Registry v2, and (opt-in) write records under an owner-signed session grant. Works with any MCP-compatible client (Claude Code, Cursor, custom agents).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"agt-mcp": "dist/index.js"
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
},
|
|
22
22
|
"repository": {
|
|
23
23
|
"type": "git",
|
|
24
|
-
"url": "https://github.com/ds1/agt-site.git",
|
|
24
|
+
"url": "git+https://github.com/ds1/agt-site.git",
|
|
25
25
|
"directory": "packages/mcp"
|
|
26
26
|
},
|
|
27
27
|
"homepage": "https://agtnames.com/docs/claude-code",
|
|
@@ -32,9 +32,11 @@
|
|
|
32
32
|
"prepublishOnly": "node scripts/check-publish-deps.mjs && npm run build && npm test"
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
|
-
"@agtnames/resolver": "^1.0.
|
|
35
|
+
"@agtnames/resolver": "^1.0.3",
|
|
36
36
|
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
37
|
-
"zod": "^3.25.0"
|
|
37
|
+
"zod": "^3.25.0",
|
|
38
|
+
"@agtnames/countersign": "^0.1.0",
|
|
39
|
+
"viem": "^2.56.0"
|
|
38
40
|
},
|
|
39
41
|
"devDependencies": {
|
|
40
42
|
"@types/node": "^22.0.0",
|