@ni-c/imap-mcp 0.2.0 → 0.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.
Files changed (52) hide show
  1. package/README.md +122 -31
  2. package/dist/analyze.d.ts +22 -3
  3. package/dist/analyze.js +202 -23
  4. package/dist/analyze.js.map +1 -1
  5. package/dist/attachments.d.ts +25 -0
  6. package/dist/attachments.js +20 -2
  7. package/dist/attachments.js.map +1 -1
  8. package/dist/audit.d.ts +7 -0
  9. package/dist/audit.js +11 -3
  10. package/dist/audit.js.map +1 -1
  11. package/dist/config.d.ts +21 -0
  12. package/dist/config.js +44 -5
  13. package/dist/config.js.map +1 -1
  14. package/dist/errors.js.map +1 -1
  15. package/dist/imap.js.map +1 -1
  16. package/dist/index.js +32 -5
  17. package/dist/index.js.map +1 -1
  18. package/dist/output-schema.d.ts +62 -0
  19. package/dist/output-schema.js +81 -0
  20. package/dist/output-schema.js.map +1 -0
  21. package/dist/resources.d.ts +1 -1
  22. package/dist/resources.js +1 -1
  23. package/dist/resources.js.map +1 -1
  24. package/dist/result.d.ts +32 -5
  25. package/dist/result.js +128 -24
  26. package/dist/result.js.map +1 -1
  27. package/dist/schema.d.ts +13 -1
  28. package/dist/schema.js +20 -2
  29. package/dist/schema.js.map +1 -1
  30. package/dist/server.d.ts +1 -1
  31. package/dist/server.js +30 -5
  32. package/dist/server.js.map +1 -1
  33. package/dist/tools/annotations.d.ts +32 -0
  34. package/dist/tools/annotations.js +33 -0
  35. package/dist/tools/annotations.js.map +1 -0
  36. package/dist/tools/catalogue.d.ts +2 -2
  37. package/dist/tools/read.d.ts +1 -1
  38. package/dist/tools/read.js +345 -50
  39. package/dist/tools/read.js.map +1 -1
  40. package/dist/tools/write.d.ts +3 -3
  41. package/dist/tools/write.js +165 -38
  42. package/dist/tools/write.js.map +1 -1
  43. package/package.json +15 -11
  44. package/dist/approval.d.ts +0 -45
  45. package/dist/approval.js +0 -69
  46. package/dist/approval.js.map +0 -1
  47. package/dist/confirm.d.ts +0 -59
  48. package/dist/confirm.js +0 -92
  49. package/dist/confirm.js.map +0 -1
  50. package/dist/tool-filter.d.ts +0 -45
  51. package/dist/tool-filter.js +0 -171
  52. package/dist/tool-filter.js.map +0 -1
package/dist/approval.js DELETED
@@ -1,69 +0,0 @@
1
- import { confirmationPrompt, renderDetails, } from './confirm.js';
2
- import { ToolInputError } from './errors.js';
3
- import { textResult } from './result.js';
4
- /** How long the server waits for the human to answer the dialog. */
5
- const ELICITATION_TIMEOUT_MS = 5 * 60 * 1000;
6
- /**
7
- * Asks a human before an irreversible operation.
8
- *
9
- * Why this exists next to {@link ConfirmationStore}: the confirmation token is
10
- * not a human-in-the-loop gate and never was. It is returned inside a tool
11
- * result, which means the model reads it and can call again in the same turn
12
- * without anyone seeing the dialog. That still catches a model that widens the
13
- * target set by accident, but a model which has been talked into deleting the
14
- * mailbox will happily call twice.
15
- *
16
- * MCP elicitation closes that hole: the request goes to the client, which shows
17
- * it to the person sitting there, and the model cannot answer on their behalf.
18
- * Clients that do not support it fall back to the token, because refusing to
19
- * work at all would push people towards turning the guard off entirely.
20
- */
21
- export async function requestApproval(server, confirmations, request) {
22
- if (server.server.getClientCapabilities()?.elicitation !== undefined) {
23
- return elicit(server, request);
24
- }
25
- if (confirmations.consume(request.resourceKey, request.token)) {
26
- return { approved: true };
27
- }
28
- return {
29
- approved: false,
30
- result: textResult(`${confirmationPrompt(request.what, confirmations.issue(request.resourceKey), confirmations.ttlMinutes, request.consequence, request.details ?? [])}\n\nNote: this client cannot ask the user directly, so this check only ` +
31
- 'proves the call was made twice with the same arguments. A human should ' +
32
- 'read the line above before you continue.'),
33
- };
34
- }
35
- async function elicit(server, request) {
36
- let response;
37
- try {
38
- response = await server.server.elicitInput({
39
- // Server-side facts only: no subject, sender or body reaches this
40
- // string. It is rendered to a human, but it is composed by us — and
41
- // the caller-chosen names go through renderDetails rather than into
42
- // the sentence, so none of it is a place to hide an instruction.
43
- message: `${request.what}\n\n${request.consequence}` +
44
- renderDetails(request.details ?? []),
45
- requestedSchema: {
46
- type: 'object',
47
- properties: {
48
- confirm: {
49
- type: 'boolean',
50
- title: 'Proceed?',
51
- description: 'Tick to allow this operation, leave it to cancel.',
52
- },
53
- },
54
- required: ['confirm'],
55
- },
56
- }, { timeout: ELICITATION_TIMEOUT_MS });
57
- }
58
- catch (error) {
59
- // A timeout, a client that advertised the capability but cannot deliver, a
60
- // dropped connection: all of them mean nobody said yes.
61
- const reason = error instanceof Error ? error.message : String(error);
62
- throw new ToolInputError(`imap-mcp: could not obtain confirmation from the user (${reason}). Nothing was changed.`);
63
- }
64
- if (response.action !== 'accept' || response.content?.confirm !== true) {
65
- throw new ToolInputError('imap-mcp: the user declined. Nothing was changed.');
66
- }
67
- return { approved: true };
68
- }
69
- //# sourceMappingURL=approval.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"approval.js","sourceRoot":"","sources":["../src/approval.ts"],"names":[],"mappings":"AAEA,OAAO,EACL,kBAAkB,EAClB,aAAa,GAGd,MAAM,cAAc,CAAC;AACtB,OAAO,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAC7C,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAIzC,oEAAoE;AACpE,MAAM,sBAAsB,GAAG,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC;AA0B7C;;;;;;;;;;;;;;GAcG;AACH,MAAM,CAAC,KAAK,UAAU,eAAe,CACnC,MAAiB,EACjB,aAAgC,EAChC,OAAwB;IAExB,IAAI,MAAM,CAAC,MAAM,CAAC,qBAAqB,EAAE,EAAE,WAAW,KAAK,SAAS,EAAE,CAAC;QACrE,OAAO,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACjC,CAAC;IAED,IAAI,aAAa,CAAC,OAAO,CAAC,OAAO,CAAC,WAAW,EAAE,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;QAC9D,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;IAC5B,CAAC;IACD,OAAO;QACL,QAAQ,EAAE,KAAK;QACf,MAAM,EAAE,UAAU,CAChB,GAAG,kBAAkB,CACnB,OAAO,CAAC,IAAI,EACZ,aAAa,CAAC,KAAK,CAAC,OAAO,CAAC,WAAW,CAAC,EACxC,aAAa,CAAC,UAAU,EACxB,OAAO,CAAC,WAAW,EACnB,OAAO,CAAC,OAAO,IAAI,EAAE,CACtB,yEAAyE;YACxE,yEAAyE;YACzE,0CAA0C,CAC7C;KACF,CAAC;AACJ,CAAC;AAED,KAAK,UAAU,MAAM,CACnB,MAAiB,EACjB,OAAwB;IAExB,IAAI,QAAQ,CAAC;IACb,IAAI,CAAC;QACH,QAAQ,GAAG,MAAM,MAAM,CAAC,MAAM,CAAC,WAAW,CACxC;YACE,kEAAkE;YAClE,oEAAoE;YACpE,oEAAoE;YACpE,iEAAiE;YACjE,OAAO,EACL,GAAG,OAAO,CAAC,IAAI,OAAO,OAAO,CAAC,WAAW,EAAE;gBAC3C,aAAa,CAAC,OAAO,CAAC,OAAO,IAAI,EAAE,CAAC;YACtC,eAAe,EAAE;gBACf,IAAI,EAAE,QAAQ;gBACd,UAAU,EAAE;oBACV,OAAO,EAAE;wBACP,IAAI,EAAE,SAAS;wBACf,KAAK,EAAE,UAAU;wBACjB,WAAW,EAAE,mDAAmD;qBACjE;iBACF;gBACD,QAAQ,EAAE,CAAC,SAAS,CAAC;aACtB;SACF,EACD,EAAE,OAAO,EAAE,sBAAsB,EAAE,CACpC,CAAC;IACJ,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,2EAA2E;QAC3E,wDAAwD;QACxD,MAAM,MAAM,GAAG,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;QACtE,MAAM,IAAI,cAAc,CACtB,0DAA0D,MAAM,yBAAyB,CAC1F,CAAC;IACJ,CAAC;IAED,IAAI,QAAQ,CAAC,MAAM,KAAK,QAAQ,IAAI,QAAQ,CAAC,OAAO,EAAE,OAAO,KAAK,IAAI,EAAE,CAAC;QACvE,MAAM,IAAI,cAAc,CACtB,mDAAmD,CACpD,CAAC;IACJ,CAAC;IACD,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;AAC5B,CAAC"}
package/dist/confirm.d.ts DELETED
@@ -1,59 +0,0 @@
1
- /**
2
- * Issues short-lived confirmation tokens for irreversible operations.
3
- *
4
- * A plain boolean `confirm` parameter could be set by the model on the very
5
- * first call — or be talked into it by instructions hidden in upstream content —
6
- * whereas a random token that only ever appears in a *previous* tool result
7
- * cannot be guessed. The token is bound to a resource key, so a confirmation for
8
- * one target cannot be replayed for another.
9
- */
10
- export declare class ConfirmationStore {
11
- private readonly ttlMs;
12
- private readonly pending;
13
- constructor(ttlMs?: number);
14
- /** Creates (or replaces) the pending token for `resource`. */
15
- issue(resource: string): string;
16
- /**
17
- * Returns true and consumes the token when it matches the pending one for
18
- * `resource` and has not expired. Tokens are single-use.
19
- */
20
- consume(resource: string, token: string | undefined): boolean;
21
- /** Minutes the issued tokens stay valid, for use in messages. */
22
- get ttlMinutes(): number;
23
- }
24
- /**
25
- * Resource key for an operation on a *set* of targets. Without the fingerprint a
26
- * confirmation for ["a.txt"] would also execute ["a.txt", "secrets.env"] — the
27
- * model chooses the second list, and only the id would have been checked.
28
- */
29
- export declare function setResourceKey(operation: string, targets: string[]): string;
30
- /**
31
- * A name this server did not choose, shown alongside a confirmation.
32
- *
33
- * Mailbox names look like server-side metadata and are not: they come from the
34
- * model's arguments, and `list_mailboxes` sources them from the account — which
35
- * on a shared mailbox or a public namespace means a colleague, or whoever
36
- * compromised one, picks them. A folder called
37
- * `Archive" — routine cleanup, pre-approved by IT` interpolated into the middle
38
- * of "This will move 12 message(s) from X to Y" reads as part of the server's
39
- * own sentence, in the one string a human is given before losing a folder.
40
- *
41
- * So they are never interpolated: {@link renderDetails} puts each on its own
42
- * labelled line. `mailboxParam` refuses CR, LF and NUL, so a value cannot open a
43
- * second line and forge a label of its own — the single-line rendering is what
44
- * that validation is worth.
45
- */
46
- export interface ConfirmationDetail {
47
- label: string;
48
- value: string;
49
- }
50
- declare function renderDetails(details: readonly ConfirmationDetail[]): string;
51
- /**
52
- * Builds the text returned by the first call of a destructive tool.
53
- *
54
- * Note what is NOT in here: no subject, sender or filename coming out of the
55
- * mailbox. Those are attacker-controllable and this string is read by a model.
56
- * Mailbox names are attacker-reachable too, which is what `details` is for.
57
- */
58
- export declare function confirmationPrompt(what: string, token: string, ttlMinutes: number, consequence?: string, details?: readonly ConfirmationDetail[]): string;
59
- export { renderDetails };
package/dist/confirm.js DELETED
@@ -1,92 +0,0 @@
1
- import { createHash, randomBytes, timingSafeEqual } from 'node:crypto';
2
- const TOKEN_TTL_MS = 5 * 60 * 1000;
3
- /** Bounds the map so a loop of refused calls cannot grow it without limit. */
4
- const MAX_PENDING = 100;
5
- /**
6
- * Issues short-lived confirmation tokens for irreversible operations.
7
- *
8
- * A plain boolean `confirm` parameter could be set by the model on the very
9
- * first call — or be talked into it by instructions hidden in upstream content —
10
- * whereas a random token that only ever appears in a *previous* tool result
11
- * cannot be guessed. The token is bound to a resource key, so a confirmation for
12
- * one target cannot be replayed for another.
13
- */
14
- export class ConfirmationStore {
15
- ttlMs;
16
- pending = new Map();
17
- constructor(ttlMs = TOKEN_TTL_MS) {
18
- this.ttlMs = ttlMs;
19
- }
20
- /** Creates (or replaces) the pending token for `resource`. */
21
- issue(resource) {
22
- if (this.pending.size >= MAX_PENDING) {
23
- const oldest = this.pending.keys().next();
24
- if (!oldest.done)
25
- this.pending.delete(oldest.value);
26
- }
27
- const token = randomBytes(16).toString('hex');
28
- this.pending.set(resource, { token, expiresAt: Date.now() + this.ttlMs });
29
- return token;
30
- }
31
- /**
32
- * Returns true and consumes the token when it matches the pending one for
33
- * `resource` and has not expired. Tokens are single-use.
34
- */
35
- consume(resource, token) {
36
- const entry = this.pending.get(resource);
37
- if (entry === undefined || token === undefined)
38
- return false;
39
- const supplied = Buffer.from(token);
40
- const expected = Buffer.from(entry.token);
41
- // Constant-time comparison. Guessing 128 random bits through a timing
42
- // side channel is not a realistic attack on a local tool, but the safe
43
- // comparison costs one line and removes the question.
44
- const matches = supplied.length === expected.length &&
45
- timingSafeEqual(supplied, expected);
46
- if (!matches)
47
- return false;
48
- // Delete on any match, expired or not. Leaving a matched-but-dead entry
49
- // behind kept it competing for space with live ones, and the eviction in
50
- // issue() is insertion-order rather than LRU, so a long-lived key could be
51
- // dropped ahead of a token that was already spent.
52
- this.pending.delete(resource);
53
- return Date.now() < entry.expiresAt;
54
- }
55
- /** Minutes the issued tokens stay valid, for use in messages. */
56
- get ttlMinutes() {
57
- return Math.round(this.ttlMs / 60_000);
58
- }
59
- }
60
- /**
61
- * Resource key for an operation on a *set* of targets. Without the fingerprint a
62
- * confirmation for ["a.txt"] would also execute ["a.txt", "secrets.env"] — the
63
- * model chooses the second list, and only the id would have been checked.
64
- */
65
- export function setResourceKey(operation, targets) {
66
- const fingerprint = createHash('sha256')
67
- .update(JSON.stringify([...targets].sort()))
68
- .digest('hex')
69
- .slice(0, 16);
70
- return `${operation}:${fingerprint}`;
71
- }
72
- function renderDetails(details) {
73
- if (details.length === 0)
74
- return '';
75
- return ('\n\nNames below are supplied by the caller, not by this server:\n' +
76
- details.map((d) => ` ${d.label}: ${d.value}`).join('\n'));
77
- }
78
- /**
79
- * Builds the text returned by the first call of a destructive tool.
80
- *
81
- * Note what is NOT in here: no subject, sender or filename coming out of the
82
- * mailbox. Those are attacker-controllable and this string is read by a model.
83
- * Mailbox names are attacker-reachable too, which is what `details` is for.
84
- */
85
- export function confirmationPrompt(what, token, ttlMinutes, consequence = 'The operation is irreversible.', details = []) {
86
- return (`This will ${what}. ${consequence}` +
87
- `${renderDetails(details)}\n\n` +
88
- `To proceed, call this tool again with confirm_token="${token}".\n` +
89
- `The token is valid for ${ttlMinutes} minutes and can be used once.`);
90
- }
91
- export { renderDetails };
92
- //# sourceMappingURL=confirm.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"confirm.js","sourceRoot":"","sources":["../src/confirm.ts"],"names":[],"mappings":"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"}
@@ -1,45 +0,0 @@
1
- import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
- /**
3
- * A tool list that names something this server does not have.
4
- *
5
- * Thrown rather than `process.exit(1)`: `createServer` is called in-process by
6
- * the tests, and an exiting constructor cannot be tested. `src/index.ts` turns
7
- * it back into an exit code.
8
- */
9
- export declare class ToolFilterError extends Error {
10
- constructor(message: string);
11
- }
12
- export interface ToolFilter {
13
- /** False when neither variable was set — then nothing is wrapped at all. */
14
- readonly active: boolean;
15
- /** The tools that survive. Only meaningful while `active`. */
16
- readonly selected: ReadonlySet<string>;
17
- }
18
- /**
19
- * Reads `<PREFIX>_ALLOW_TOOLS` / `<PREFIX>_DENY_TOOLS` and works out which
20
- * tools survive.
21
- *
22
- * Every entry has to match at least one tool in the catalogue. An entry that
23
- * matches nothing is fatal rather than ignored, because the failure it produces
24
- * otherwise — a tool quietly missing from `tools/list` — is invisible: nobody
25
- * looks for the cause of an absence in an environment variable.
26
- */
27
- export declare function buildToolFilter(config: {
28
- allowTools: string | undefined;
29
- denyTools: string | undefined;
30
- readOnly: boolean;
31
- }): ToolFilter;
32
- /**
33
- * Makes `server` register only the tools the filter selected.
34
- *
35
- * The tool is registered and then removed again rather than skipped. Skipping
36
- * looks cheaper and breaks one case: the SDK installs its `tools/list` handler
37
- * from inside the registration path, so a server whose every tool was skipped
38
- * would answer `tools/list` with "method not found" instead of an empty list.
39
- * `remove()` deletes the entry from the SDK's tool map outright, which makes a
40
- * filtered tool answer `Tool X not found` — exactly what a tool the read-only
41
- * mode never registered already does. `disable()` would be wrong: it hides the
42
- * tool from `tools/list` but still answers a call with "disabled", which is the
43
- * advertising-a-refusal this server avoids everywhere else.
44
- */
45
- export declare function installToolFilter(server: McpServer, filter: ToolFilter): void;
@@ -1,171 +0,0 @@
1
- import { ALL_TOOLS, ESSENTIAL_TOOLS, READ_TOOLS } from './tools/catalogue.js';
2
- /**
3
- * A tool list that names something this server does not have.
4
- *
5
- * Thrown rather than `process.exit(1)`: `createServer` is called in-process by
6
- * the tests, and an exiting constructor cannot be tested. `src/index.ts` turns
7
- * it back into an exit code.
8
- */
9
- export class ToolFilterError extends Error {
10
- constructor(message) {
11
- super(message);
12
- this.name = 'ToolFilterError';
13
- }
14
- }
15
- /** The `essential` preset is spelled out here so it cannot collide with a tool name. */
16
- const PRESET = 'essential';
17
- /**
18
- * Splits a comma-separated value into entries.
19
- *
20
- * Empty entries are dropped, so `a,,b` and a trailing comma are both fine, and
21
- * a value that is empty or only whitespace counts as *unset* — `X_ALLOW_TOOLS=`
22
- * in a compose file must not mean "allow nothing". Entries are lowercased: the
23
- * catalogue is entirely lowercase, so this is lossless, and a shell that
24
- * upper-cased a name should not take the server down.
25
- */
26
- function entriesOf(raw) {
27
- if (raw === undefined)
28
- return undefined;
29
- const entries = raw
30
- .split(',')
31
- .map((entry) => entry.trim().toLowerCase())
32
- .filter((entry) => entry.length > 0);
33
- return entries.length > 0 ? entries : undefined;
34
- }
35
- /**
36
- * Expands one entry to the catalogue tools it names.
37
- *
38
- * A pattern is a literal prefix plus exactly one trailing `*`. Anything else is
39
- * rejected outright: `*_zone` and `list_*_x` look plausible, match nothing, and
40
- * would otherwise be silent forever.
41
- */
42
- function expand(entry, variable) {
43
- const star = entry.indexOf('*');
44
- if (star !== -1) {
45
- if (star !== entry.length - 1) {
46
- throw new ToolFilterError(`${variable}: "${entry}" is not a valid entry — a pattern is a prefix ` +
47
- 'followed by a single trailing "*", for example "list_*". Everything ' +
48
- 'else is an exact tool name.');
49
- }
50
- const prefix = entry.slice(0, -1);
51
- return ALL_TOOLS.filter((tool) => tool.startsWith(prefix));
52
- }
53
- return ALL_TOOLS.filter((tool) => tool === entry);
54
- }
55
- /** The full catalogue, for the "these are the names that exist" half of an error. */
56
- function catalogueList() {
57
- return [...ALL_TOOLS].sort().join(', ');
58
- }
59
- /**
60
- * Reads `<PREFIX>_ALLOW_TOOLS` / `<PREFIX>_DENY_TOOLS` and works out which
61
- * tools survive.
62
- *
63
- * Every entry has to match at least one tool in the catalogue. An entry that
64
- * matches nothing is fatal rather than ignored, because the failure it produces
65
- * otherwise — a tool quietly missing from `tools/list` — is invisible: nobody
66
- * looks for the cause of an absence in an environment variable.
67
- */
68
- export function buildToolFilter(config) {
69
- const allow = entriesOf(config.allowTools);
70
- const deny = entriesOf(config.denyTools);
71
- if (allow === undefined && deny === undefined) {
72
- return { active: false, selected: new Set() };
73
- }
74
- // What would be registered without any filter. In read-only mode the write
75
- // tools never reach `registerTool`, but they stay in the catalogue so that a
76
- // name from that half is answered with "read-only suppresses it", never with
77
- // "no such tool".
78
- const registered = new Set(config.readOnly ? READ_TOOLS : ALL_TOOLS);
79
- // Set when an allow entry named real tools and read-only suppressed all of
80
- // them, so that "nothing is left" can name the reason rather than shrugging.
81
- let suppressedByReadOnly = false;
82
- let selected;
83
- if (allow === undefined) {
84
- selected = new Set(registered);
85
- }
86
- else {
87
- selected = new Set();
88
- for (const entry of allow) {
89
- if (entry === PRESET) {
90
- // Preset members are not names the operator typed, so a member that
91
- // read-only suppresses is dropped silently rather than being an error.
92
- for (const tool of ESSENTIAL_TOOLS) {
93
- if (registered.has(tool))
94
- selected.add(tool);
95
- }
96
- continue;
97
- }
98
- const matches = expand(entry, 'IMAP_ALLOW_TOOLS');
99
- if (matches.length === 0) {
100
- throw new ToolFilterError(`IMAP_ALLOW_TOOLS: no tool matches "${entry}". ` +
101
- `Valid tools: ${catalogueList()}. "${PRESET}" selects the curated preset.`);
102
- }
103
- const survivors = matches.filter((tool) => registered.has(tool));
104
- if (survivors.length === 0) {
105
- if (entry.endsWith('*')) {
106
- // A pattern is a template, not a claim about one tool: warn, continue.
107
- console.error(`imap-mcp: IMAP_ALLOW_TOOLS: "${entry}" matches only write ` +
108
- 'tools, which IMAP_READ_ONLY suppresses — it contributes nothing.');
109
- suppressedByReadOnly = true;
110
- continue;
111
- }
112
- // An exact name, though, was typed by someone who believes it is exposed.
113
- throw new ToolFilterError(`IMAP_ALLOW_TOOLS: "${entry}" is a write tool, but IMAP_READ_ONLY ` +
114
- 'is set — it is never registered. Remove it from IMAP_ALLOW_TOOLS, ' +
115
- `or unset IMAP_READ_ONLY. Available in read-only mode: ${[...READ_TOOLS].sort().join(', ')}.`);
116
- }
117
- for (const tool of survivors)
118
- selected.add(tool);
119
- }
120
- }
121
- for (const entry of deny ?? []) {
122
- // Deny lists are written defensively — "never expose delete_*, whatever
123
- // else is on" — so matching nothing that survives is fine. Matching nothing
124
- // in the catalogue is still a typo.
125
- const matches = expand(entry, 'IMAP_DENY_TOOLS');
126
- if (matches.length === 0) {
127
- throw new ToolFilterError(`IMAP_DENY_TOOLS: no tool matches "${entry}". Valid tools: ${catalogueList()}.`);
128
- }
129
- for (const tool of matches)
130
- selected.delete(tool);
131
- }
132
- if (selected.size === 0) {
133
- throw new ToolFilterError(suppressedByReadOnly
134
- ? 'IMAP_ALLOW_TOOLS selects only write tools, but IMAP_READ_ONLY is ' +
135
- 'set — the server would start with an empty tool list.'
136
- : 'IMAP_ALLOW_TOOLS/IMAP_DENY_TOOLS leave no tools registered — the ' +
137
- 'server would start with an empty tool list.');
138
- }
139
- return { active: true, selected };
140
- }
141
- /**
142
- * Makes `server` register only the tools the filter selected.
143
- *
144
- * The tool is registered and then removed again rather than skipped. Skipping
145
- * looks cheaper and breaks one case: the SDK installs its `tools/list` handler
146
- * from inside the registration path, so a server whose every tool was skipped
147
- * would answer `tools/list` with "method not found" instead of an empty list.
148
- * `remove()` deletes the entry from the SDK's tool map outright, which makes a
149
- * filtered tool answer `Tool X not found` — exactly what a tool the read-only
150
- * mode never registered already does. `disable()` would be wrong: it hides the
151
- * tool from `tools/list` but still answers a call with "disabled", which is the
152
- * advertising-a-refusal this server avoids everywhere else.
153
- */
154
- export function installToolFilter(server, filter) {
155
- if (!filter.active)
156
- return;
157
- const register = server.registerTool.bind(server);
158
- // An object literal rather than a plain function so the method picks up the
159
- // SDK's generic signature by contextual typing — the call sites keep their
160
- // typed handler arguments and nothing needs a cast.
161
- const wrapper = {
162
- registerTool(name, config, cb) {
163
- const tool = register(name, config, cb);
164
- if (!filter.selected.has(name))
165
- tool.remove();
166
- return tool;
167
- },
168
- };
169
- server.registerTool = wrapper.registerTool;
170
- }
171
- //# sourceMappingURL=tool-filter.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"tool-filter.js","sourceRoot":"","sources":["../src/tool-filter.ts"],"names":[],"mappings":"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"}