@amalgm/tools 0.1.7-preview.34746901886 → 0.1.7-preview.34938534039

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 (47) hide show
  1. package/PURPOSE.md +29 -1
  2. package/README.md +29 -5
  3. package/dist/api-driver.d.ts +2 -1
  4. package/dist/api-driver.js +67 -48
  5. package/dist/cli-driver.js +2 -1
  6. package/dist/commands.js +3 -2
  7. package/dist/connection-auth.d.ts +11 -0
  8. package/dist/connection-auth.js +98 -0
  9. package/dist/connection-source.d.ts +23 -0
  10. package/dist/connection-source.js +62 -0
  11. package/dist/connection-types.d.ts +34 -0
  12. package/dist/connection-types.js +1 -0
  13. package/dist/connections.d.ts +14 -0
  14. package/dist/connections.js +98 -0
  15. package/dist/credential-operations.d.ts +7 -0
  16. package/dist/credential-operations.js +37 -0
  17. package/dist/definition.js +10 -3
  18. package/dist/http-mutations.d.ts +63 -0
  19. package/dist/http-mutations.js +41 -0
  20. package/dist/http.d.ts +2 -3
  21. package/dist/http.js +9 -3
  22. package/dist/index.d.ts +5 -0
  23. package/dist/index.js +2 -0
  24. package/dist/input.js +7 -32
  25. package/dist/loadout.d.ts +3 -0
  26. package/dist/loadout.js +23 -0
  27. package/dist/mcp-driver.d.ts +16 -0
  28. package/dist/mcp-driver.js +44 -0
  29. package/dist/mcp-search.d.ts +8 -0
  30. package/dist/mcp-search.js +51 -0
  31. package/dist/mcp-types.d.ts +13 -0
  32. package/dist/mcp-types.js +1 -0
  33. package/dist/mcp.js +23 -19
  34. package/dist/oauth.d.ts +29 -0
  35. package/dist/oauth.js +131 -0
  36. package/dist/prepared-catalog.d.ts +3 -1
  37. package/dist/prepared-catalog.js +20 -1
  38. package/dist/results.d.ts +4 -1
  39. package/dist/results.js +21 -16
  40. package/dist/selection.js +3 -0
  41. package/dist/toolbox-view.d.ts +4 -1
  42. package/dist/toolbox-view.js +8 -2
  43. package/dist/toolbox.d.ts +7 -0
  44. package/dist/toolbox.js +25 -14
  45. package/dist/types.d.ts +27 -20
  46. package/docs/SHELL_INTEGRATION.md +8 -3
  47. package/package.json +5 -4
@@ -0,0 +1,37 @@
1
+ const stores = new WeakMap();
2
+ /** All credential writes share an order; a stale refresh never recreates a deletion. */
3
+ export function credentialOperations(store) {
4
+ const existing = stores.get(store);
5
+ if (existing)
6
+ return existing;
7
+ const pending = new Map();
8
+ async function ordered(reference, operation) {
9
+ const previous = pending.get(reference) || Promise.resolve();
10
+ const current = previous.catch(() => undefined).then(operation);
11
+ pending.set(reference, current);
12
+ try {
13
+ return await current;
14
+ }
15
+ finally {
16
+ if (pending.get(reference) === current)
17
+ pending.delete(reference);
18
+ }
19
+ }
20
+ const wrapper = {
21
+ get: (reference) => ordered(reference, () => store.get(reference)),
22
+ set: (reference, value) => ordered(reference, () => store.set(reference, value)),
23
+ delete: (reference) => ordered(reference, () => store.delete(reference)),
24
+ replace: (reference, expected, value) => ordered(reference, async () => {
25
+ if (await store.get(reference) !== expected)
26
+ return false;
27
+ if (value === undefined)
28
+ await store.delete(reference);
29
+ else
30
+ await store.set(reference, value);
31
+ return true;
32
+ }),
33
+ };
34
+ stores.set(store, wrapper);
35
+ stores.set(wrapper, wrapper);
36
+ return wrapper;
37
+ }
@@ -68,15 +68,16 @@ function normalizeSource(source) {
68
68
  ...(refs(source.secretEnv, 'source.secretEnv') ? { secretEnv: refs(source.secretEnv, 'source.secretEnv') } : {}),
69
69
  };
70
70
  }
71
- if (source.type === 'api') {
72
- const url = new URL(string(source.baseUrl, 'source.baseUrl'));
71
+ if (source.type === 'api' || source.type === 'graphql') {
72
+ const url = new URL(string(source.type === 'api' ? source.baseUrl : source.url, 'source URL'));
73
73
  if (!['http:', 'https:'].includes(url.protocol))
74
74
  throw new Error('source.baseUrl must use http or https');
75
75
  if (url.username || url.password)
76
76
  throw new Error('source.baseUrl cannot contain credentials');
77
77
  return {
78
- type: 'api', baseUrl: url.toString(), ...base(source),
78
+ ...(source.type === 'api' ? { type: 'api', baseUrl: url.toString() } : { type: 'graphql', url: url.toString() }), ...base(source),
79
79
  ...(refs(source.secretHeaders, 'source.secretHeaders') ? { secretHeaders: refs(source.secretHeaders, 'source.secretHeaders') } : {}),
80
+ ...(refs(source.secretQuery, 'source.secretQuery') ? { secretQuery: refs(source.secretQuery, 'source.secretQuery') } : {}),
80
81
  };
81
82
  }
82
83
  if (source.type === 'mcp') {
@@ -102,6 +103,7 @@ function normalizeSource(source) {
102
103
  ...(source.serverName ? { serverName: string(source.serverName, 'source.serverName') } : {}),
103
104
  ...(hostRoute(source.route) ? { route: hostRoute(source.route) } : {}),
104
105
  ...(refs(source.secretHeaders, 'source.secretHeaders') ? { secretHeaders: refs(source.secretHeaders, 'source.secretHeaders') } : {}),
106
+ ...(refs(source.secretQuery, 'source.secretQuery') ? { secretQuery: refs(source.secretQuery, 'source.secretQuery') } : {}),
105
107
  ...(refs(source.secretEnv, 'source.secretEnv') ? { secretEnv: refs(source.secretEnv, 'source.secretEnv') } : {}),
106
108
  };
107
109
  }
@@ -125,6 +127,11 @@ function target(type, value) {
125
127
  return { ...(strings(raw.args, 'action.target.args') ? { args: strings(raw.args, 'action.target.args') } : {}) };
126
128
  if (type === 'api')
127
129
  return { method: string(raw.method, 'action.target.method').toUpperCase(), path: string(raw.path, 'action.target.path') };
130
+ if (type === 'graphql')
131
+ return {
132
+ document: string(raw.document, 'action.target.document'),
133
+ ...(raw.operationName ? { operationName: string(raw.operationName, 'action.target.operationName') } : {}),
134
+ };
128
135
  return { name: string(raw.name, 'action.target.name') };
129
136
  }
130
137
  function normalizeDefinition(definition, existing, now = new Date().toISOString()) {
@@ -0,0 +1,63 @@
1
+ import type { Toolbox } from './toolbox.js';
2
+ export declare function mutateToolbox(toolbox: Toolbox, method: string, pathname: string, value: unknown): Promise<{
3
+ status: number;
4
+ body: {
5
+ url: string;
6
+ state: string;
7
+ toolId: string;
8
+ };
9
+ } | {
10
+ status: number;
11
+ body: {
12
+ ok: boolean;
13
+ tool?: undefined;
14
+ actions?: undefined;
15
+ deleted?: undefined;
16
+ error?: undefined;
17
+ };
18
+ } | {
19
+ status: number;
20
+ body: {
21
+ ok: boolean;
22
+ tool: {
23
+ type: "cli" | "api" | "graphql" | "mcp";
24
+ origin: import("./types.js").Origin;
25
+ status: import("./types.js").Status;
26
+ owner: string;
27
+ createdAt: string;
28
+ updatedAt: string;
29
+ description?: string | undefined;
30
+ name: string;
31
+ id: string;
32
+ policy?: Record<string, unknown> | undefined;
33
+ metadata?: Record<string, unknown> | undefined;
34
+ appId?: string | undefined;
35
+ source: import("./types.js").ToolSource;
36
+ display?: Record<string, unknown> | undefined;
37
+ };
38
+ actions: import("./types.js").ActionRecord[];
39
+ deleted?: undefined;
40
+ error?: undefined;
41
+ };
42
+ } | {
43
+ status: number;
44
+ body: {
45
+ ok: boolean;
46
+ deleted: boolean;
47
+ tool?: undefined;
48
+ actions?: undefined;
49
+ error?: undefined;
50
+ };
51
+ } | {
52
+ status: number;
53
+ body: import("./types.js").ToolResult;
54
+ } | {
55
+ status: number;
56
+ body: {
57
+ error: string;
58
+ ok?: undefined;
59
+ tool?: undefined;
60
+ actions?: undefined;
61
+ deleted?: undefined;
62
+ };
63
+ }>;
@@ -0,0 +1,41 @@
1
+ export async function mutateToolbox(toolbox, method, pathname, value) {
2
+ if (!value || typeof value !== 'object' || Array.isArray(value))
3
+ throw new Error('Toolbox request must be an object');
4
+ const body = value;
5
+ if (pathname === '/toolbox/oauth/authorize' && method === 'POST') {
6
+ return { status: 200, body: await toolbox.authorization.begin(body) };
7
+ }
8
+ if (pathname === '/toolbox/oauth/callback' && method === 'POST') {
9
+ await toolbox.authorization.complete({ state: String(body.state || ''), code: String(body.code || '') });
10
+ return { status: 200, body: { ok: true } };
11
+ }
12
+ if (pathname === '/toolbox/tools') {
13
+ if (method === 'POST') {
14
+ const result = await toolbox.connect(body);
15
+ return { status: 200, body: { ok: true, tool: { ...result.tool, type: result.tool.source.type }, actions: result.actions } };
16
+ }
17
+ if (method === 'PATCH') {
18
+ const toolId = typeof body.id === 'string' ? body.id : '';
19
+ if (body.status === 'disabled') {
20
+ const result = await toolbox.disconnect(toolId);
21
+ return { status: 200, body: { ok: true, tool: { ...result.tool, type: result.tool.source.type }, actions: result.actions } };
22
+ }
23
+ if (body.status === 'enabled') {
24
+ const current = toolbox.get(toolId);
25
+ if (!current)
26
+ throw new Error(`Unknown tool: ${toolId}`);
27
+ const result = await toolbox.connect({ definition: { ...current.tool, actions: current.actions } });
28
+ return { status: 200, body: { ok: true, tool: { ...result.tool, type: result.tool.source.type }, actions: result.actions } };
29
+ }
30
+ throw new Error('Tool status must be enabled or disabled; reconnect with a complete definition to edit it');
31
+ }
32
+ if (method === 'DELETE') {
33
+ await toolbox.remove(typeof body.id === 'string' ? body.id : '');
34
+ return { status: 200, body: { ok: true, deleted: true } };
35
+ }
36
+ }
37
+ if (pathname === '/toolbox/actions/call' && method === 'POST') {
38
+ return { status: 200, body: await toolbox.call(typeof body.id === 'string' ? body.id : '', body.input) };
39
+ }
40
+ return { status: 405, body: { error: 'method not allowed' } };
41
+ }
package/dist/http.d.ts CHANGED
@@ -12,9 +12,8 @@ import type { Catalog, ToolboxOptions } from './types.js';
12
12
  * - `GET /toolbox/actions?id=...` returns one action;
13
13
  * - domain errors are JSON with status 400; unknown routes are 404.
14
14
  *
15
- * Mutation routes remain deliberately absent until their engine record fields
16
- * are represented losslessly by the SDK. Returning 405 is honest; translating
17
- * them approximately would turn a visible parity gap into data loss.
15
+ * Mutation routes consume current SDK definitions. The authenticated composing
16
+ * host supplies the Toolbox owner; request bodies cannot select one.
18
17
  */
19
18
  type Json = Record<string, unknown>;
20
19
  interface ToolboxHttpServer {
package/dist/http.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { createServer } from 'node:http';
2
2
  import { toolboxIndexDocument } from './artifacts.js';
3
3
  import { Toolbox } from './toolbox.js';
4
+ import { mutateToolbox } from './http-mutations.js';
4
5
  function sourceWithoutType(source) {
5
6
  const { type: _type, ...value } = source;
6
7
  return value;
@@ -25,8 +26,13 @@ function catalogDocument(catalog) {
25
26
  }
26
27
  async function readBody(req) {
27
28
  const chunks = [];
28
- for await (const chunk of req)
29
+ let size = 0;
30
+ for await (const chunk of req) {
31
+ size += Buffer.byteLength(chunk);
32
+ if (size > 2 * 1024 * 1024)
33
+ throw new Error('Toolbox request exceeds 2 MiB');
29
34
  chunks.push(Buffer.from(chunk));
35
+ }
30
36
  if (chunks.length === 0)
31
37
  return {};
32
38
  return JSON.parse(Buffer.concat(chunks).toString('utf8'));
@@ -45,8 +51,8 @@ function createToolboxHttpServer(options = {}) {
45
51
  try {
46
52
  if (method !== 'GET') {
47
53
  if (url.pathname.startsWith('/toolbox')) {
48
- await readBody(req);
49
- send(405, { error: 'method not allowed' });
54
+ const result = await mutateToolbox(toolbox, method, url.pathname, await readBody(req));
55
+ send(result.status, result.body);
50
56
  return;
51
57
  }
52
58
  send(404, { error: 'not found' });
package/dist/index.d.ts CHANGED
@@ -17,5 +17,10 @@ export type { EmailDelivery, EmailDeliveryReceipt, EmailNotificationRequest, Not
17
17
  export { createProxyEmailDelivery } from './proxy-email.js';
18
18
  export type { ProxyEmailDeliveryOptions } from './proxy-email.js';
19
19
  export { Toolbox } from './toolbox.js';
20
+ export { createMcpDriver } from './mcp-driver.js';
21
+ export type { McpClientPort, McpConnectionEffect } from './mcp-driver.js';
22
+ export type { ConnectToolInput, ConnectionCredentials, OAuthCredential, ToolCredentialStore } from './connection-types.js';
23
+ export { normalizeLoadout } from './loadout.js';
24
+ export type { ToolAuthorizationInput } from './oauth.js';
20
25
  export type { ToolboxView as PreparedToolbox } from './toolbox-view.js';
21
26
  export type * from './types.js';
package/dist/index.js CHANGED
@@ -12,3 +12,5 @@ export { createMcpServer } from './mcp-server.js';
12
12
  export { Notifications, createNotificationsDriver, notificationToolDefinition, } from './notifications.js';
13
13
  export { createProxyEmailDelivery } from './proxy-email.js';
14
14
  export { Toolbox } from './toolbox.js';
15
+ export { createMcpDriver } from './mcp-driver.js';
16
+ export { normalizeLoadout } from './loadout.js';
package/dist/input.js CHANGED
@@ -1,43 +1,18 @@
1
+ import { AjvJsonSchemaValidator } from '@modelcontextprotocol/sdk/validation/ajv';
1
2
  function objectInput(value) {
2
3
  if (value === undefined)
3
4
  return {};
4
- if (!value || typeof value !== 'object' || Array.isArray(value)) {
5
+ if (!value || typeof value !== 'object' || Array.isArray(value))
5
6
  throw new Error('Tool input must be an object');
6
- }
7
7
  return value;
8
8
  }
9
- function matchesType(value, type) {
10
- if (type === 'array')
11
- return Array.isArray(value);
12
- if (type === 'integer')
13
- return Number.isInteger(value);
14
- if (type === 'number')
15
- return typeof value === 'number' && Number.isFinite(value);
16
- if (type === 'object')
17
- return !!value && typeof value === 'object' && !Array.isArray(value);
18
- if (type === 'null')
19
- return value === null;
20
- return typeof value === type;
21
- }
22
9
  function validateInput(schema, value) {
23
10
  const input = objectInput(value);
24
- for (const key of schema.required || []) {
25
- if (!(key in input))
26
- throw new Error(`Tool input is missing required property: ${key}`);
27
- }
28
- if (schema.additionalProperties === false) {
29
- const known = new Set(Object.keys(schema.properties || {}));
30
- const extra = Object.keys(input).find((key) => !known.has(key));
31
- if (extra)
32
- throw new Error(`Tool input contains unknown property: ${extra}`);
33
- }
34
- for (const [key, property] of Object.entries(schema.properties || {})) {
35
- if (!(key in input) || !property.type)
36
- continue;
37
- if (!matchesType(input[key], property.type)) {
38
- throw new Error(`Tool input property ${key} must be ${property.type}`);
39
- }
40
- }
11
+ // Schemas may reuse $id across unrelated tools; compilation has no global namespace.
12
+ const validate = new AjvJsonSchemaValidator().getValidator(schema);
13
+ const result = validate(input);
14
+ if (!result.valid)
15
+ throw new Error(`Invalid tool input: ${result.errorMessage}`);
41
16
  return input;
42
17
  }
43
18
  export { objectInput, validateInput };
@@ -0,0 +1,3 @@
1
+ import type { Loadout, LoadoutInput, ToolExposure } from './types.js';
2
+ export declare function normalizeLoadout(input: LoadoutInput): Loadout;
3
+ export declare function actionExposure(loadout: LoadoutInput | undefined, toolId: string, actionId: string): ToolExposure;
@@ -0,0 +1,23 @@
1
+ export function normalizeLoadout(input) {
2
+ const raw = Array.isArray(input) ? { toolIds: input } : input;
3
+ if (!raw || !Array.isArray(raw.toolIds) || raw.toolIds.some((id) => typeof id !== 'string' || !id)) {
4
+ throw new Error('Loadout requires toolIds');
5
+ }
6
+ const exposure = {};
7
+ if (raw.exposure !== undefined) {
8
+ if (!raw.exposure || typeof raw.exposure !== 'object' || Array.isArray(raw.exposure)) {
9
+ throw new Error('Loadout exposure must be a map of ids to direct, search, or hidden');
10
+ }
11
+ for (const [id, mode] of Object.entries(raw.exposure).sort(([a], [b]) => a.localeCompare(b))) {
12
+ if (!id || !['direct', 'search', 'hidden'].includes(mode))
13
+ throw new Error(`Invalid tool exposure: ${id}`);
14
+ exposure[id] = mode;
15
+ }
16
+ }
17
+ return { toolIds: [...new Set(raw.toolIds)].sort(), ...(Object.keys(exposure).length ? { exposure } : {}) };
18
+ }
19
+ export function actionExposure(loadout, toolId, actionId) {
20
+ if (!loadout || Array.isArray(loadout))
21
+ return 'direct';
22
+ return loadout.exposure?.[actionId] ?? loadout.exposure?.[toolId] ?? 'direct';
23
+ }
@@ -0,0 +1,16 @@
1
+ import type { Tool, CallToolResult } from '@modelcontextprotocol/sdk/types.js';
2
+ import type { McpSource, ToolDriver } from './types.js';
3
+ export interface McpConnectionEffect {
4
+ source: McpSource;
5
+ headers: Record<string, string>;
6
+ env: Record<string, string>;
7
+ query?: Record<string, string>;
8
+ signal?: AbortSignal;
9
+ context?: unknown;
10
+ }
11
+ /** One composing host owns upstream transports and their native effects. */
12
+ export interface McpClientPort {
13
+ list(connection: McpConnectionEffect): Promise<Tool[]>;
14
+ call(connection: McpConnectionEffect, name: string, input: Record<string, unknown>): Promise<CallToolResult>;
15
+ }
16
+ export declare function createMcpDriver(port: McpClientPort): ToolDriver;
@@ -0,0 +1,44 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { boundedResult } from './results.js';
3
+ import { resolveReferences } from './secrets.js';
4
+ function storedActionName(name) {
5
+ if (/^[a-z0-9][a-z0-9_-]{0,47}$/.test(name))
6
+ return name;
7
+ const label = name.toLowerCase().replace(/[^a-z0-9_-]+/g, '_').replace(/^[_-]+/, '').slice(0, 28) || 'action';
8
+ return `${label}-${createHash('sha256').update(name).digest('hex').slice(0, 16)}`;
9
+ }
10
+ export function createMcpDriver(port) {
11
+ async function connection(tool, options) {
12
+ if (tool.source.type !== 'mcp')
13
+ throw new Error('MCP source required');
14
+ const source = tool.source;
15
+ return {
16
+ source,
17
+ query: await resolveReferences(source.secretQuery, options.resolveSecret),
18
+ headers: await resolveReferences(source.secretHeaders, options.resolveSecret),
19
+ env: await resolveReferences(source.secretEnv, options.resolveSecret),
20
+ signal: options.signal, context: options.context,
21
+ };
22
+ }
23
+ return {
24
+ type: 'mcp',
25
+ async discover(tool, options) {
26
+ const tools = await port.list(await connection(tool, options));
27
+ return tools.map((tool) => ({
28
+ name: storedActionName(tool.name), displayName: tool.title || tool.name,
29
+ ...(tool.description ? { description: tool.description } : {}),
30
+ inputSchema: tool.inputSchema,
31
+ ...(tool.outputSchema ? { outputSchema: tool.outputSchema } : {}),
32
+ target: { name: tool.name },
33
+ ...(tool.annotations || tool._meta ? { metadata: {
34
+ ...(tool.annotations ? { annotations: tool.annotations } : {}),
35
+ ...(tool._meta ? { _meta: tool._meta } : {}),
36
+ } } : {}),
37
+ }));
38
+ },
39
+ async call({ tool, action, input, options }) {
40
+ const result = await port.call(await connection(tool, options), action.target.name, input);
41
+ return boundedResult(result, tool.source.maxOutputBytes);
42
+ },
43
+ };
44
+ }
@@ -0,0 +1,8 @@
1
+ import type { Toolbox } from './toolbox.js';
2
+ import type { ToolboxView } from './toolbox-view.js';
3
+ import type { ActionRecord, McpOptions, McpTool, ToolRecord } from './types.js';
4
+ /** Search is a presentation of the same immutable grant, never another catalog. */
5
+ export declare function searchTools(toolbox: Toolbox | ToolboxView, selected: Array<{
6
+ tool: ToolRecord;
7
+ action: ActionRecord;
8
+ }>, options: McpOptions): McpTool[];
@@ -0,0 +1,51 @@
1
+ import { mcpName } from './ids.js';
2
+ import { errorResult, structuredResult } from './results.js';
3
+ /** Search is a presentation of the same immutable grant, never another catalog. */
4
+ export function searchTools(toolbox, selected, options) {
5
+ if (!selected.length)
6
+ return [];
7
+ return [{
8
+ name: 'toolbox_search',
9
+ description: 'Find available tool actions by name or description. Use toolbox_call with a returned action id and its input schema.',
10
+ inputSchema: {
11
+ type: 'object', properties: { query: { type: 'string' } }, required: ['query'], additionalProperties: false,
12
+ },
13
+ async handler(input = {}) {
14
+ if (typeof input.query !== 'string')
15
+ return errorResult('query must be a string');
16
+ const words = input.query.toLowerCase().split(/\s+/).filter(Boolean);
17
+ const available = new Set(toolbox.callable(options.loadout).map(({ action }) => action.id));
18
+ const matches = selected.filter(({ tool, action }) => {
19
+ if (!available.has(action.id))
20
+ return false;
21
+ const text = [tool.name, action.id, action.displayName, action.description].join(' ').toLowerCase();
22
+ return words.every((word) => text.includes(word));
23
+ });
24
+ return structuredResult({
25
+ actions: matches.slice(0, 50).map(({ action }) => ({
26
+ id: action.id, name: mcpName(action), description: action.description || action.displayName || action.name,
27
+ inputSchema: action.inputSchema,
28
+ ...(action.outputSchema ? { outputSchema: action.outputSchema } : {}),
29
+ })),
30
+ hasMore: matches.length > 50,
31
+ });
32
+ },
33
+ }, {
34
+ name: 'toolbox_call',
35
+ description: 'Execute one action returned by toolbox_search with arguments matching its input schema.',
36
+ inputSchema: {
37
+ type: 'object', properties: { id: { type: 'string' }, input: { type: 'object' } },
38
+ required: ['id', 'input'], additionalProperties: false,
39
+ },
40
+ async handler(input = {}) {
41
+ if (!selected.some(({ action }) => action.id === input.id))
42
+ return errorResult('Action is unavailable');
43
+ try {
44
+ return await toolbox.call(String(input.id), input.input, options);
45
+ }
46
+ catch (error) {
47
+ return errorResult(error);
48
+ }
49
+ },
50
+ }];
51
+ }
@@ -0,0 +1,13 @@
1
+ import type { CallOptions, JsonSchema, ToolResult } from './types.js';
2
+ export interface McpTool {
3
+ annotations?: import('@modelcontextprotocol/sdk/types.js').Tool['annotations'];
4
+ _meta?: Record<string, unknown>;
5
+ name: string;
6
+ description: string;
7
+ inputSchema: JsonSchema;
8
+ outputSchema?: Record<string, unknown>;
9
+ handler(input?: Record<string, unknown>): Promise<ToolResult>;
10
+ }
11
+ export interface McpOptions extends CallOptions {
12
+ includeManagement?: boolean;
13
+ }
@@ -0,0 +1 @@
1
+ export {};
package/dist/mcp.js CHANGED
@@ -1,12 +1,9 @@
1
1
  import { mcpName } from './ids.js';
2
+ import { actionExposure } from './loadout.js';
3
+ import { searchTools } from './mcp-search.js';
2
4
  import { errorResult, structuredResult } from './results.js';
3
5
  function portable(schema) {
4
- const result = structuredClone(schema);
5
- for (const key of ['anyOf', 'oneOf', 'allOf', 'not', 'enum'])
6
- delete result[key];
7
- result.type = 'object';
8
- result.properties ||= {};
9
- return result;
6
+ return structuredClone(schema);
10
7
  }
11
8
  function managed(definition, handler) {
12
9
  return {
@@ -94,19 +91,26 @@ function managementTools(toolbox, options = {}) {
94
91
  ];
95
92
  }
96
93
  function actionTools(toolbox, options = {}) {
97
- return toolbox.callable(options.loadout).map(({ tool, action }) => ({
98
- name: mcpName(action),
99
- description: action.description || `Run ${tool.name}: ${action.displayName || action.name}.`,
100
- inputSchema: portable(action.inputSchema),
101
- async handler(input = {}) {
102
- try {
103
- return await toolbox.call(action.id, input, options);
104
- }
105
- catch (error) {
106
- return errorResult(error);
107
- }
108
- },
109
- }));
94
+ const callable = toolbox.callable(options.loadout);
95
+ const searchable = callable.filter(({ tool, action }) => (('exposure' in toolbox && toolbox.exposure(action) === 'search')
96
+ || actionExposure(options.loadout, tool.id, action.id) === 'search'));
97
+ const searchIds = new Set(searchable.map(({ action }) => action.id));
98
+ return [...callable.filter(({ action }) => !searchIds.has(action.id)).map(({ tool, action }) => ({
99
+ name: mcpName(action),
100
+ ...(action.metadata?.annotations ? { annotations: structuredClone(action.metadata.annotations) } : {}),
101
+ ...(action.metadata?._meta ? { _meta: structuredClone(action.metadata._meta) } : {}),
102
+ description: action.description || `Run ${tool.name}: ${action.displayName || action.name}.`,
103
+ inputSchema: portable(action.inputSchema),
104
+ ...(action.outputSchema ? { outputSchema: structuredClone(action.outputSchema) } : {}),
105
+ async handler(input = {}) {
106
+ try {
107
+ return await toolbox.call(action.id, input, options);
108
+ }
109
+ catch (error) {
110
+ return errorResult(error);
111
+ }
112
+ },
113
+ })), ...searchTools(toolbox, searchable, options)];
110
114
  }
111
115
  function createMcpTools(toolbox, options = {}) {
112
116
  if (options.includeManagement !== false && !('apply' in toolbox)) {
@@ -0,0 +1,29 @@
1
+ import type { OAuthDiscoveryState } from '@modelcontextprotocol/sdk/client/auth.js';
2
+ import type { ConnectToolInput, ToolCredentialStore } from './connection-types.js';
3
+ import type { ApplyResult, ToolDefinition } from './types.js';
4
+ export interface ToolAuthorizationInput {
5
+ definition: ToolDefinition;
6
+ redirectUri: string;
7
+ scope?: string;
8
+ clientId?: string;
9
+ clientSecret?: string;
10
+ /** Optional registered-provider metadata; otherwise use MCP discovery. */
11
+ metadata?: OAuthDiscoveryState['authorizationServerMetadata'];
12
+ }
13
+ export declare const authorizationReference: (toolId: string) => string;
14
+ /** MCP's OAuth flow, with all pending and granted secrets in the host vault. */
15
+ export declare class ToolAuthorization {
16
+ #private;
17
+ private readonly store;
18
+ private readonly connect;
19
+ constructor(store: ToolCredentialStore | undefined, connect: (input: ConnectToolInput, guard: () => Promise<void>) => Promise<ApplyResult>);
20
+ begin(input: ToolAuthorizationInput): Promise<{
21
+ url: string;
22
+ state: string;
23
+ toolId: string;
24
+ }>;
25
+ complete(input: {
26
+ state: string;
27
+ code: string;
28
+ }): Promise<ApplyResult>;
29
+ }