@fennecstudio/x-tkn-mcp 1.2.0 → 1.3.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 +29 -9
- package/dist/index.js +81 -15
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -6,9 +6,10 @@ Lets an AI agent hand a secret to a person as a one-time link, with no way to re
|
|
|
6
6
|
npx -y @fennecstudio/x-tkn-mcp
|
|
7
7
|
```
|
|
8
8
|
|
|
9
|
-
- **No read tool.** The server has `create_secret`, `check_status
|
|
10
|
-
`revoke_secret`. Once a secret is handed over, the agent
|
|
11
|
-
was opened and can cancel it. It cannot see the secret
|
|
9
|
+
- **No read tool.** The server has `create_secret`, `check_status`,
|
|
10
|
+
`revoke_secret` and `claim_sandbox`. Once a secret is handed over, the agent
|
|
11
|
+
can see whether it was opened and can cancel it. It cannot see the secret
|
|
12
|
+
again.
|
|
12
13
|
- **Encrypted on your machine.** `create_secret` encrypts with AES-256-GCM
|
|
13
14
|
before anything is sent. X-TKN stores only ciphertext. The key travels in the
|
|
14
15
|
link's `#fragment`, which browsers never send to a server.
|
|
@@ -37,12 +38,14 @@ Servers that start together share one sandbox: while one mints, it holds a
|
|
|
37
38
|
Every result on a sandbox key ends with a line giving that end.
|
|
38
39
|
- When it ends, the next call starts a new sandbox and says so. Codes from the
|
|
39
40
|
old one no longer work with `check_status` or `revoke_secret`.
|
|
40
|
-
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
41
|
+
- To keep a sandbox, call `claim_sandbox`. It returns a one-time link for the
|
|
42
|
+
person who should keep it, and the key never leaves this machine. A sandbox
|
|
43
|
+
has to be claimed before it can be upgraded. The sandbox key still ends on
|
|
44
|
+
schedule after a claim, so a claimed account goes on with a Developer key
|
|
45
|
+
created for it and set here.
|
|
44
46
|
- Once a sandbox has spent its operations, the server does not start another,
|
|
45
|
-
because that would lose the old one's codes.
|
|
47
|
+
because that would lose the old one's codes. Call `claim_sandbox`, or set a
|
|
48
|
+
key.
|
|
46
49
|
|
|
47
50
|
**For a key that stays**, Developer keys are free: sign up at
|
|
48
51
|
[x-tkn.com](https://x-tkn.com) and create one in the account console. A key in
|
|
@@ -84,7 +87,8 @@ Encrypts a secret and returns a link that opens it once.
|
|
|
84
87
|
| `recipientEmail` | no | The recipient has to enter a 6-digit code sent to this address. Team and above. Not with `passphrase`. |
|
|
85
88
|
| `note` | no | For your own records in the X-TKN console. Stored unencrypted and never shown to the recipient. |
|
|
86
89
|
|
|
87
|
-
Returns the `link`, a `code` for
|
|
90
|
+
Returns the `link`, a `code` for `check_status` and `revoke_secret`, and
|
|
91
|
+
`expiresAt`.
|
|
88
92
|
|
|
89
93
|
Send a passphrase by a different channel than the link. Anyone holding both can
|
|
90
94
|
open the secret.
|
|
@@ -103,6 +107,22 @@ opens spends a whole sandbox in under nine minutes.
|
|
|
103
107
|
Takes the `code`. Stops the link working, immediately and for good. A secret
|
|
104
108
|
that was already opened stays opened.
|
|
105
109
|
|
|
110
|
+
### `claim_sandbox`
|
|
111
|
+
|
|
112
|
+
Takes no arguments. Returns a `claimUrl` and its `expiresAt`: a one-time link a
|
|
113
|
+
person opens to claim the sandbox this server's key belongs to, so the account
|
|
114
|
+
outlives its 24 hours. Claiming keeps the sandbox's secrets.
|
|
115
|
+
|
|
116
|
+
Give the link only to the person who should keep the sandbox. It works once,
|
|
117
|
+
until `expiresAt`: 30 minutes, or the sandbox's end if that is sooner. Calling
|
|
118
|
+
`claim_sandbox` again voids the earlier link. It is not metered.
|
|
119
|
+
|
|
120
|
+
The sandbox key still ends when the sandbox would have, claimed or not. For a
|
|
121
|
+
key that lasts, create a Developer key for the account in the account console
|
|
122
|
+
and set `X_TKN_API_KEY`. On a key that is not an unclaimed sandbox (one already
|
|
123
|
+
claimed, merged into another account or ended, or a Developer key), it says so
|
|
124
|
+
and claims nothing.
|
|
125
|
+
|
|
106
126
|
## What X-TKN sees
|
|
107
127
|
|
|
108
128
|
The ciphertext, the expiry, your `note`, and, if you set them, a hash of the
|
package/dist/index.js
CHANGED
|
@@ -216,9 +216,9 @@ var XTkn = class _XTkn {
|
|
|
216
216
|
* clicking a link in an email.
|
|
217
217
|
*
|
|
218
218
|
* The account is capped and temporary: 100 operations and 24 hours. It may
|
|
219
|
-
* create `secret` tokens, none of which outlives it. {@link XTkn.claim}
|
|
220
|
-
* turns it into an ordinary
|
|
221
|
-
* and no expiry.
|
|
219
|
+
* create `secret` tokens, none of which outlives it. {@link XTkn.claim}, or
|
|
220
|
+
* the link {@link XTkn.createClaimLink} returns, turns it into an ordinary
|
|
221
|
+
* Developer account with 10,000 operations a month and no expiry.
|
|
222
222
|
*
|
|
223
223
|
* `account.apiKey` is returned once and cannot be read back. Persist it if
|
|
224
224
|
* the process may restart before the account is claimed. It stops working at
|
|
@@ -250,9 +250,14 @@ var XTkn = class _XTkn {
|
|
|
250
250
|
* client authenticates as is the one claimed — there is no account id to
|
|
251
251
|
* pass and no way to name somebody else's.
|
|
252
252
|
*
|
|
253
|
-
* Existing tokens
|
|
254
|
-
*
|
|
255
|
-
*
|
|
253
|
+
* Existing tokens survive. The sandbox key still ends when the sandbox would
|
|
254
|
+
* have, so create a Developer key for the account. When that email already
|
|
255
|
+
* owns an account under the same sign-in, the sandbox is merged into it
|
|
256
|
+
* instead: the result has `merged: true` and that account's `customerId`,
|
|
257
|
+
* with no `class` or `monthlyOperations`, and this client's key now acts as
|
|
258
|
+
* that account until it lapses. Throws {@link XTknRequestError} with a 409
|
|
259
|
+
* when the email's account is under a different sign-in; sign in with that
|
|
260
|
+
* one, then claim again.
|
|
256
261
|
*
|
|
257
262
|
* @example
|
|
258
263
|
* ```ts
|
|
@@ -269,6 +274,32 @@ var XTkn = class _XTkn {
|
|
|
269
274
|
headers: { "X-Firebase-Token": firebaseIdToken }
|
|
270
275
|
});
|
|
271
276
|
}
|
|
277
|
+
/**
|
|
278
|
+
* Get a one-time link a person opens to claim this sandbox account.
|
|
279
|
+
*
|
|
280
|
+
* {@link XTkn.claim} needs the person's sign-in in your own code. This needs
|
|
281
|
+
* only the sandbox key, so an agent holding the key can hand its person the
|
|
282
|
+
* link instead. The claim is the same either way: existing tokens survive,
|
|
283
|
+
* and the sandbox key still ends when the sandbox would have.
|
|
284
|
+
*
|
|
285
|
+
* The link works once, until `expiresAt`: 30 minutes, or the sandbox's end
|
|
286
|
+
* if that is sooner. Minting another voids it. Not counted against the
|
|
287
|
+
* sandbox's operations.
|
|
288
|
+
*
|
|
289
|
+
* Throws {@link XTknError} with a 409 when this key's account is not an
|
|
290
|
+
* unclaimed sandbox: already claimed, merged into another account, ended, or
|
|
291
|
+
* never a sandbox.
|
|
292
|
+
*
|
|
293
|
+
* @example
|
|
294
|
+
* ```ts
|
|
295
|
+
* const { client } = await XTkn.createSandbox()
|
|
296
|
+
* const { claimUrl, expiresAt } = await client.createClaimLink()
|
|
297
|
+
* console.log(`Open this before ${expiresAt} to keep the sandbox: ${claimUrl}`)
|
|
298
|
+
* ```
|
|
299
|
+
*/
|
|
300
|
+
async createClaimLink() {
|
|
301
|
+
return this.request("POST", "/accounts/claim");
|
|
302
|
+
}
|
|
272
303
|
/**
|
|
273
304
|
* Create a token.
|
|
274
305
|
*
|
|
@@ -729,19 +760,18 @@ function buildSecretLink(code2, key, options = {}) {
|
|
|
729
760
|
// src/tools.ts
|
|
730
761
|
var SECRET_TYPE = "secret";
|
|
731
762
|
var SIGNUP_URL = "https://x-tkn.com";
|
|
732
|
-
var SANDBOX_CLAIM_ENDPOINT = "POST /accounts/claim";
|
|
733
763
|
var SANDBOX_EXPIRY_REFUSAL = "Sandbox accounts cannot mint tokens that outlive them.";
|
|
734
764
|
var NO_KEY_MESSAGE = `No X-TKN API key. Set ${API_KEY_ENV_VARS[0]} in this MCP server's environment. To start now with no signup, POST to ${DEFAULT_BASE_URL}/accounts/sandbox for a sandbox key. It lasts 24 hours, and so does every secret it creates. For a key that stays, Developer keys are free: sign up at ${SIGNUP_URL} and create one in the account console.`;
|
|
735
|
-
var SANDBOX_KEY_MESSAGE = `This is a sandbox key. A sandbox account is deleted with its secrets 24 hours after it was created, so no secret it creates can expire later than that. Leave expiresIn out to get the latest expiry it allows. For longer,
|
|
765
|
+
var SANDBOX_KEY_MESSAGE = `This is a sandbox key. A sandbox account is deleted with its secrets 24 hours after it was created, so no secret it creates can expire later than that. Leave expiresIn out to get the latest expiry it allows. For longer, call claim_sandbox and give the link it returns to the person who should keep the account, or set ${API_KEY_ENV_VARS[0]} to a free Developer key from ${SIGNUP_URL}.`;
|
|
736
766
|
function sandboxStartupMessage(path) {
|
|
737
767
|
return `No ${API_KEY_ENV_VARS[0]} set, so this server uses a sandbox: the first tool call reuses the one saved in ${path}, or starts a new one that lasts 24 hours. For a key that stays, set ${API_KEY_ENV_VARS[0]} to a free Developer key from ${SIGNUP_URL}.`;
|
|
738
768
|
}
|
|
739
769
|
function sandboxLine(sandbox, path) {
|
|
740
770
|
const ends = `Sandbox key: it and its secrets end at ${sandbox.expiresAt}.`;
|
|
741
771
|
if (sandbox.unsaved) {
|
|
742
|
-
return `${ends} It could not be saved to ${path} (${sandbox.unsaved}), so
|
|
772
|
+
return `${ends} It could not be saved to ${path} (${sandbox.unsaved}), so this server loses it when it stops. To keep the sandbox, call claim_sandbox before then.`;
|
|
743
773
|
}
|
|
744
|
-
return `${ends} To keep
|
|
774
|
+
return `${ends} Its key is saved in ${path}. To keep the sandbox, call claim_sandbox for a link to give the person who should have it. A sandbox has to be claimed before it can be upgraded.`;
|
|
745
775
|
}
|
|
746
776
|
var SANDBOX_RENEWED_MESSAGE = `A new sandbox started: the last sandbox key expired or X-TKN refused it. Codes create_secret returned before this belong to the old sandbox, and check_status and revoke_secret cannot reach them. If that sandbox was claimed, the account lives on: create a Developer key for it in the account console at ${SIGNUP_URL} and set ${API_KEY_ENV_VARS[0]}.`;
|
|
747
777
|
var SANDBOX_MINT_REFUSED_MESSAGE = `X-TKN is not starting another sandbox from this address for now. Set ${API_KEY_ENV_VARS[0]} in this MCP server's environment to a free Developer key from ${SIGNUP_URL}, or try again in a few minutes.`;
|
|
@@ -779,6 +809,16 @@ async function checkStatus(client, code2) {
|
|
|
779
809
|
async function revokeSecret(client, code2) {
|
|
780
810
|
return statusOf(await client.revokeToken(code2));
|
|
781
811
|
}
|
|
812
|
+
var CLAIMED_KEY_STILL_ENDS = `A claimed sandbox key still ends when the sandbox would have. For a key that lasts, create a Developer key for the account in the account console at ${SIGNUP_URL} and set ${API_KEY_ENV_VARS[0]}.`;
|
|
813
|
+
var NOT_AN_UNCLAIMED_SANDBOX_MESSAGE = "This key is not an unclaimed sandbox, so there is nothing to claim: it is a sandbox key that was already claimed, merged into another account or has ended, or not a sandbox key at all. " + CLAIMED_KEY_STILL_ENDS;
|
|
814
|
+
async function claimSandbox(client) {
|
|
815
|
+
try {
|
|
816
|
+
return await client.createClaimLink();
|
|
817
|
+
} catch (err) {
|
|
818
|
+
if (err instanceof XTknError && err.status === 409) throw new XTknError(NOT_AN_UNCLAIMED_SANDBOX_MESSAGE, 0);
|
|
819
|
+
throw err;
|
|
820
|
+
}
|
|
821
|
+
}
|
|
782
822
|
function statusOf(token) {
|
|
783
823
|
const uses = token.uses ?? 0;
|
|
784
824
|
const opened = token.isUsed ?? (token.maxUses != null && uses >= token.maxUses);
|
|
@@ -794,7 +834,7 @@ function statusOf(token) {
|
|
|
794
834
|
function describeError(err, origin = "key") {
|
|
795
835
|
if (origin === "mint" && err instanceof XTknRateLimitError) return SANDBOX_MINT_REFUSED_MESSAGE;
|
|
796
836
|
if (origin === "sandbox" && err instanceof XTknRateLimitError) {
|
|
797
|
-
return `X-TKN is refusing more requests on this sandbox key (${err.message}). To go on,
|
|
837
|
+
return `X-TKN is refusing more requests on this sandbox key (${err.message}). To go on, call claim_sandbox and give the link it returns to a person: claiming the sandbox lifts its cap. Or set ${API_KEY_ENV_VARS[0]} to a free Developer key from ${SIGNUP_URL}.`;
|
|
798
838
|
}
|
|
799
839
|
if (origin === "sandbox" && err instanceof XTknAuthError && err.status === 401) {
|
|
800
840
|
return `X-TKN refused this server's sandbox key (${err.message}). Set ${API_KEY_ENV_VARS[0]} to a free Developer key from ${SIGNUP_URL}.`;
|
|
@@ -802,8 +842,7 @@ function describeError(err, origin = "key") {
|
|
|
802
842
|
if (err instanceof XTknPaymentRequiredError) {
|
|
803
843
|
const plan = err.requiredPlan ? `the ${err.requiredPlan} plan` : "a paid plan";
|
|
804
844
|
if (err.upgradeMethod === "claim") {
|
|
805
|
-
|
|
806
|
-
return `${err.message} This needs ${plan}, and this is a sandbox key: a person has to claim the account at ${claim} before it can be upgraded.`;
|
|
845
|
+
return `${err.message} This needs ${plan}, and this is a sandbox key: a person has to claim the account before it can be upgraded. Call claim_sandbox for the link that does it.`;
|
|
807
846
|
}
|
|
808
847
|
const where = err.upgradeUrl ? ` A person can upgrade here: ${err.upgradeUrl}` : "";
|
|
809
848
|
return `${err.message} This needs ${plan}.${where}`;
|
|
@@ -1020,7 +1059,7 @@ function isUnauthorized(err) {
|
|
|
1020
1059
|
}
|
|
1021
1060
|
|
|
1022
1061
|
// src/version.ts
|
|
1023
|
-
var VERSION = "1.
|
|
1062
|
+
var VERSION = "1.3.0";
|
|
1024
1063
|
|
|
1025
1064
|
// src/env.ts
|
|
1026
1065
|
var USER_AGENT = `x-tkn-mcp/${VERSION}`;
|
|
@@ -1061,7 +1100,8 @@ var INSTRUCTIONS = [
|
|
|
1061
1100
|
// key should start building, not stop to ask a person to sign up, and since
|
|
1062
1101
|
// 1.2.0 it need not even fetch a key: `connectionFrom` mints one.
|
|
1063
1102
|
"With no X_TKN_API_KEY set, this server gets a sandbox key itself on the first tool call: no signup, and it creates real secrets for 24 hours, long enough to build and test the integration.",
|
|
1064
|
-
|
|
1103
|
+
"To keep a sandbox past its 24 hours, call claim_sandbox and give the link it returns to the person who should keep it.",
|
|
1104
|
+
`Its key still ends on schedule after a claim. For a key that stays, a person signs up at ${SIGNUP_URL} for a free Developer key.`
|
|
1065
1105
|
].join(" ");
|
|
1066
1106
|
var durationField = z.number().int().min(0);
|
|
1067
1107
|
var expiresIn = z.object({
|
|
@@ -1134,6 +1174,19 @@ function createXtknMcpServer(options) {
|
|
|
1134
1174
|
},
|
|
1135
1175
|
({ code: code2 }) => run(connection, async (client) => status(await revokeSecret(client, code2)))
|
|
1136
1176
|
);
|
|
1177
|
+
server.registerTool(
|
|
1178
|
+
"claim_sandbox",
|
|
1179
|
+
{
|
|
1180
|
+
title: "Get a link that keeps this sandbox",
|
|
1181
|
+
description: "Get a one-time link a person opens to claim the sandbox this server's key belongs to, so the account outlives its 24 hours and keeps its secrets. Give the link only to the person who should keep it. It works once, for 30 minutes or until the sandbox ends if that is sooner, and calling this again voids the earlier link. The sandbox key itself still ends on schedule: a claimed account goes on with a Developer key created for it. Not metered. On a key that is not an unclaimed sandbox it says so and claims nothing.",
|
|
1182
|
+
outputSchema: {
|
|
1183
|
+
claimUrl: z.string().describe("The one-time link for the person who should keep the sandbox."),
|
|
1184
|
+
expiresAt: z.string().describe("When the link stops working if nobody has used it.")
|
|
1185
|
+
},
|
|
1186
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true }
|
|
1187
|
+
},
|
|
1188
|
+
() => run(connection, async (client) => claimLink(await claimSandbox(client)))
|
|
1189
|
+
);
|
|
1137
1190
|
return server;
|
|
1138
1191
|
}
|
|
1139
1192
|
async function run(connection, work) {
|
|
@@ -1155,6 +1208,19 @@ function created(secret) {
|
|
|
1155
1208
|
lines.push(`Code for check_status and revoke_secret: ${secret.code}`);
|
|
1156
1209
|
return { content: [{ type: "text", text: lines.join("\n") }], structuredContent: { ...secret } };
|
|
1157
1210
|
}
|
|
1211
|
+
function claimLink(link) {
|
|
1212
|
+
const lines = [
|
|
1213
|
+
`Claim link: ${link.claimUrl}`,
|
|
1214
|
+
"Give it to the person who should keep this sandbox, and to nobody else: anyone holding it can claim the sandbox.",
|
|
1215
|
+
`It works once, until ${link.expiresAt}: 30 minutes, or the sandbox's end if that is sooner. Calling claim_sandbox again voids it.`,
|
|
1216
|
+
"Claiming keeps the sandbox's secrets.",
|
|
1217
|
+
CLAIMED_KEY_STILL_ENDS
|
|
1218
|
+
];
|
|
1219
|
+
return {
|
|
1220
|
+
content: [{ type: "text", text: lines.join("\n") }],
|
|
1221
|
+
structuredContent: { claimUrl: link.claimUrl, expiresAt: link.expiresAt }
|
|
1222
|
+
};
|
|
1223
|
+
}
|
|
1158
1224
|
var STATUS_TEXT = {
|
|
1159
1225
|
waiting: "Waiting: not opened yet.",
|
|
1160
1226
|
opened: "Opened: the recipient has it, and the link no longer works.",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fennecstudio/x-tkn-mcp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"mcpName": "io.github.fennecstudio/x-tkn-mcp",
|
|
5
5
|
"description": "MCP server for X-TKN: hand a secret to a person as a one-time link, encrypted on your machine. No read tool.",
|
|
6
6
|
"keywords": [
|