@thinger-io/cli-core 0.4.2 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +207 -0
- package/README.md +82 -31
- package/lib/auth.js +25 -1
- package/lib/bucket-export.js +91 -0
- package/lib/cli.js +106 -50
- package/lib/commands/_shared.js +89 -1
- package/lib/commands/api.js +90 -0
- package/lib/commands/arguments.js +62 -0
- package/lib/commands/bucket.js +52 -44
- package/lib/commands/dashboard.js +5 -2
- package/lib/commands/data.js +539 -0
- package/lib/commands/device.js +128 -241
- package/lib/commands/grammar.js +279 -0
- package/lib/commands/product.js +104 -214
- package/lib/commands/profile.js +1 -0
- package/lib/commands/resources.js +975 -0
- package/lib/dashboard-init.js +4 -3
- package/lib/dashboard.js +10 -2
- package/lib/mcp/catalog.js +302 -64
- package/lib/mcp/core-types.js +36 -0
- package/lib/mcp/helpers.js +23 -2
- package/lib/mcp/naming.js +20 -0
- package/lib/mcp/registry.js +18 -67
- package/lib/mcp/resource-hooks.js +19 -5
- package/lib/mcp/server.js +43 -11
- package/lib/mcp/tools-resource.js +1234 -330
- package/lib/mcp/types/bucket-data.js +151 -0
- package/lib/mcp/{tools-dashboards.js → types/dashboard-apps.js} +99 -37
- package/lib/mcp/types/device-resource.js +117 -0
- package/lib/mcp/types/doc.js +35 -0
- package/lib/mcp/types/plugin-call.js +75 -0
- package/lib/mcp/types/product-api.js +44 -0
- package/lib/mcp/types/profile.js +37 -0
- package/lib/mcp/types/storage-file.js +170 -0
- package/lib/openapi.js +18 -0
- package/lib/product-apply.js +121 -0
- package/lib/raw-api.js +111 -0
- package/lib/storage.js +41 -13
- package/lib/tokens.js +16 -2
- package/package.json +1 -1
- package/lib/mcp/tools-buckets.js +0 -130
- package/lib/mcp/tools-devices.js +0 -134
- package/lib/mcp/tools-misc.js +0 -48
- package/lib/mcp/tools-plugins.js +0 -63
- package/lib/mcp/tools-products.js +0 -52
- package/lib/mcp/tools-storage.js +0 -173
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
import { bucketData } from './types/bucket-data.js';
|
|
3
|
+
import { dashboardActions } from './types/dashboard-apps.js';
|
|
4
|
+
import { deviceResource, overview } from './types/device-resource.js';
|
|
5
|
+
import { doc } from './types/doc.js';
|
|
6
|
+
import { pluginCall } from './types/plugin-call.js';
|
|
7
|
+
import { apiCall } from './types/product-api.js';
|
|
8
|
+
import { profile } from './types/profile.js';
|
|
9
|
+
import { storageFile } from './types/storage-file.js';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* What every CLI on the core serves next to the catalog, through the same
|
|
13
|
+
* `resource_*` tools: data and files as types (a bucket's time series, a
|
|
14
|
+
* storage's files, a connected device's resources), the guides and the
|
|
15
|
+
* profiles, and the actions on catalog types that are more than one
|
|
16
|
+
* request (or run on this machine). They take the shape of the types and
|
|
17
|
+
* actions a CLI adds (tools-resource.js), with the server operations each
|
|
18
|
+
* runs (`ops`, `op`), or `local` when it runs here.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/** @type {Record<string, import('./tools-resource.js').LocalType>} */
|
|
22
|
+
export const CORE_TYPES = {
|
|
23
|
+
bucket_data: bucketData,
|
|
24
|
+
storage_file: storageFile,
|
|
25
|
+
device_resource: deviceResource,
|
|
26
|
+
doc,
|
|
27
|
+
profile,
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/** @type {Record<string, Record<string, import('./tools-resource.js').LocalAction>>} */
|
|
31
|
+
export const CORE_ACTIONS = {
|
|
32
|
+
device: { overview },
|
|
33
|
+
product_profile_api: { call: apiCall },
|
|
34
|
+
plugin: { call: pluginCall },
|
|
35
|
+
dashboard: dashboardActions,
|
|
36
|
+
};
|
package/lib/mcp/helpers.js
CHANGED
|
@@ -1,7 +1,28 @@
|
|
|
1
1
|
// @ts-check
|
|
2
2
|
import { getProfile, withActiveProfile } from '../config.js';
|
|
3
3
|
import { inputError } from '../errors.js';
|
|
4
|
-
import {
|
|
4
|
+
import { opRef } from './naming.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Key of the structured answer a tool result carries next to its text, for
|
|
8
|
+
* the CLI commands that run the same handlers (`--json`). A symbol: it
|
|
9
|
+
* never reaches the MCP client.
|
|
10
|
+
*/
|
|
11
|
+
export const DATA = Symbol('data');
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Set on the arguments of a call a CLI command makes (runTool): the
|
|
15
|
+
* answer is read by a person, so records go without the `ids` an agent
|
|
16
|
+
* passes back.
|
|
17
|
+
*/
|
|
18
|
+
export const CLI = Symbol('cli');
|
|
19
|
+
|
|
20
|
+
/** A text tool result, with `data` for the CLI's --json when given. */
|
|
21
|
+
export const textResult = (t, data) => ({
|
|
22
|
+
content: [{ type: 'text', text: t }],
|
|
23
|
+
isError: false,
|
|
24
|
+
...(data === undefined ? {} : { [DATA]: data }),
|
|
25
|
+
});
|
|
5
26
|
|
|
6
27
|
/** Run `fn` against `profile` (when supplied) so callers can target any
|
|
7
28
|
* configured environment per tool call without persisting the change.
|
|
@@ -11,7 +32,7 @@ export async function withProfile(profile, fn) {
|
|
|
11
32
|
if (!profile) return fn();
|
|
12
33
|
if (!getProfile(profile)) {
|
|
13
34
|
throw inputError(
|
|
14
|
-
`Unknown profile: ${profile}.
|
|
35
|
+
`Unknown profile: ${profile}. ${opRef('profile', 'list')} lists the available ones.`,
|
|
15
36
|
);
|
|
16
37
|
}
|
|
17
38
|
return withActiveProfile(profile, fn);
|
package/lib/mcp/naming.js
CHANGED
|
@@ -9,6 +9,10 @@ import { identity } from '../identity.js';
|
|
|
9
9
|
* `{family:token}` (→ `thinger_token_*`); the registry expands both once
|
|
10
10
|
* the identity is known. Handlers building messages at call time use
|
|
11
11
|
* `toolName()` directly.
|
|
12
|
+
*
|
|
13
|
+
* The tools are the eight `resource_*` ones: an operation is a type and a
|
|
14
|
+
* verb or action, written `{op:<type>.<verb|action>}` in the texts (the
|
|
15
|
+
* resource tools expand it, checking it exists) and `opRef()` in code.
|
|
12
16
|
*/
|
|
13
17
|
|
|
14
18
|
const TOOL_REF = /\{(tool|family):([a-z0-9_]+)\}/g;
|
|
@@ -42,3 +46,19 @@ export function expandToolRefs(text, known, where) {
|
|
|
42
46
|
return toolName(base);
|
|
43
47
|
});
|
|
44
48
|
}
|
|
49
|
+
|
|
50
|
+
/** Verbs with a tool of their own; anything else is an action of resource_call. */
|
|
51
|
+
const VERB_TOOLS = new Set(['describe', 'list', 'get', 'create', 'update', 'delete', 'clone']);
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* How an operation is called: `<prefix>_resource_list type "bucket_data"`,
|
|
55
|
+
* `<prefix>_resource_call type "device" action "send"`.
|
|
56
|
+
*
|
|
57
|
+
* @param {string} type
|
|
58
|
+
* @param {string} op A verb or an action.
|
|
59
|
+
*/
|
|
60
|
+
export function opRef(type, op) {
|
|
61
|
+
return VERB_TOOLS.has(op)
|
|
62
|
+
? `${toolName(`resource_${op}`)} type "${type}"`
|
|
63
|
+
: `${toolName('resource_call')} type "${type}" action "${op}"`;
|
|
64
|
+
}
|
package/lib/mcp/registry.js
CHANGED
|
@@ -1,15 +1,8 @@
|
|
|
1
1
|
// @ts-check
|
|
2
|
-
import { tools as bucketTools } from './tools-buckets.js';
|
|
3
|
-
import { tools as dashboardTools } from './tools-dashboards.js';
|
|
4
|
-
import { tools as deviceTools } from './tools-devices.js';
|
|
5
|
-
import { tools as miscTools } from './tools-misc.js';
|
|
6
|
-
import { tools as pluginTools } from './tools-plugins.js';
|
|
7
|
-
import { tools as productTools } from './tools-products.js';
|
|
8
2
|
import { resourceTools } from './tools-resource.js';
|
|
9
|
-
import { tools as storageTools } from './tools-storage.js';
|
|
10
3
|
import { docMimeType, listDocs, readDoc } from '../docs.js';
|
|
11
4
|
import { identity } from '../identity.js';
|
|
12
|
-
import { expandToolRefs, toolName } from './naming.js';
|
|
5
|
+
import { expandToolRefs, opRef, toolName } from './naming.js';
|
|
13
6
|
|
|
14
7
|
/**
|
|
15
8
|
* @typedef {object} ToolOptions
|
|
@@ -19,77 +12,35 @@ import { expandToolRefs, toolName } from './naming.js';
|
|
|
19
12
|
* Resource types the CLI adds, served by the same `resource_*` tools.
|
|
20
13
|
* @property {Record<string, Record<string, import('./tools-resource.js').LocalAction>>} [actions]
|
|
21
14
|
* Actions the CLI adds to resource types (the server's or its own).
|
|
15
|
+
* @property {boolean} [readOnly] Serve only what reads (`mcp --read-only`).
|
|
22
16
|
*/
|
|
23
17
|
|
|
24
18
|
/**
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*/
|
|
31
|
-
export function genericTools(opts = {}) {
|
|
32
|
-
return [
|
|
33
|
-
...resourceTools(opts),
|
|
34
|
-
...deviceTools,
|
|
35
|
-
...productTools,
|
|
36
|
-
...bucketTools,
|
|
37
|
-
...storageTools,
|
|
38
|
-
...dashboardTools,
|
|
39
|
-
...pluginTools,
|
|
40
|
-
...miscTools,
|
|
41
|
-
];
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
// `profile_list` is the lookup of the profiles themselves, so it takes none.
|
|
45
|
-
const PROFILE_EXEMPT = new Set(['profile_list']);
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* Build the tool list a CLI serves: its own tools on top of the generic
|
|
49
|
-
* ones, every name prefixed with the identity's tool prefix, `{tool:x}`
|
|
50
|
-
* references in descriptions expanded, and the optional `profile`
|
|
51
|
-
* parameter injected so callers can target any configured environment
|
|
52
|
-
* per call without changing the saved default.
|
|
19
|
+
* Build the tool list a CLI serves: the eight `resource_*` tools over the
|
|
20
|
+
* catalog, the core's types and the CLI's, every name prefixed with the
|
|
21
|
+
* identity's tool prefix, `{tool:x}` references in descriptions expanded,
|
|
22
|
+
* and the optional `profile` parameter injected so callers can target any
|
|
23
|
+
* configured environment per call without changing the saved default.
|
|
53
24
|
*
|
|
54
25
|
* Returns fresh objects: the declarations stay untouched, so building
|
|
55
|
-
* twice (tests
|
|
26
|
+
* twice (tests) never double-prefixes anything.
|
|
56
27
|
*
|
|
57
|
-
* @param {Array<object>} [extraTools] CLI-specific tools, same shape as the
|
|
58
|
-
* generic ones; `replaces: true` swaps out the generic tool of that name.
|
|
59
28
|
* @param {ToolOptions} [opts]
|
|
29
|
+
* @returns {Array<Record<string, any>>}
|
|
60
30
|
*/
|
|
61
|
-
export function buildTools(
|
|
62
|
-
const
|
|
63
|
-
// A CLI may replace a generic tool with its own (same name, flagged
|
|
64
|
-
// `replaces: true`): its platform knows more about that operation.
|
|
65
|
-
// Without the flag a clash is a mistake and fails loudly.
|
|
66
|
-
const replaced = new Set();
|
|
67
|
-
const extraNames = new Set();
|
|
68
|
-
for (const t of extraTools) {
|
|
69
|
-
if (extraNames.has(t.name)) throw new Error(`Duplicate MCP tool name: ${t.name}`);
|
|
70
|
-
extraNames.add(t.name);
|
|
71
|
-
const isGeneric = generic.some((g) => g.name === t.name);
|
|
72
|
-
if (t.replaces && !isGeneric) {
|
|
73
|
-
throw new Error(`MCP tool "${t.name}" replaces no generic tool`);
|
|
74
|
-
}
|
|
75
|
-
if (isGeneric && !t.replaces) throw new Error(`Duplicate MCP tool name: ${t.name}`);
|
|
76
|
-
if (isGeneric) replaced.add(t.name);
|
|
77
|
-
}
|
|
78
|
-
const declared = [...generic.filter((t) => !replaced.has(t.name)), ...extraTools];
|
|
31
|
+
export function buildTools(opts = {}) {
|
|
32
|
+
const declared = resourceTools(opts);
|
|
79
33
|
const known = new Set(declared.map((t) => t.name));
|
|
80
34
|
|
|
81
35
|
return declared.map((t) => {
|
|
82
36
|
const inputSchema = expandSchema(structuredClone(t.inputSchema), known, t.name);
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
};
|
|
89
|
-
}
|
|
90
|
-
const { replaces: _replaces, ...rest } = t;
|
|
37
|
+
inputSchema.properties = inputSchema.properties || {};
|
|
38
|
+
inputSchema.properties.profile = {
|
|
39
|
+
type: 'string',
|
|
40
|
+
description: `CLI profile (server) for this call; ${opRef('profile', 'list')} lists them. Omit for the default.`,
|
|
41
|
+
};
|
|
91
42
|
return {
|
|
92
|
-
...
|
|
43
|
+
...t,
|
|
93
44
|
name: toolName(t.name),
|
|
94
45
|
description: expandToolRefs(t.description, known, t.name),
|
|
95
46
|
inputSchema,
|
|
@@ -121,7 +72,7 @@ export async function listResources() {
|
|
|
121
72
|
uri: `${prefix}://${d.id}`,
|
|
122
73
|
name: d.id,
|
|
123
74
|
title: d.title,
|
|
124
|
-
description: `${d.description || d.title} Also through ${
|
|
75
|
+
description: `${d.description || d.title} Also through ${opRef('doc', 'get')}.`,
|
|
125
76
|
mimeType: docMimeType(d.format),
|
|
126
77
|
}));
|
|
127
78
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// @ts-check
|
|
2
2
|
import { CREDENTIAL_DEVICE_TYPES, generateDeviceCredentials } from '../devices.js';
|
|
3
3
|
import { identity } from '../identity.js';
|
|
4
|
-
import { resolveExpiry, validatePermissionTree } from '../tokens.js';
|
|
4
|
+
import { resolveExpiry, resolveExpiryMs, validatePermissionTree } from '../tokens.js';
|
|
5
5
|
import { toolName } from './naming.js';
|
|
6
6
|
|
|
7
7
|
/**
|
|
@@ -15,10 +15,13 @@ import { toolName } from './naming.js';
|
|
|
15
15
|
*
|
|
16
16
|
* @typedef {object} HookContext
|
|
17
17
|
* @property {string} type
|
|
18
|
-
* @property {string}
|
|
19
|
-
* @property {string} [
|
|
18
|
+
* @property {Record<string, string>} ids Every id of the call, by name.
|
|
19
|
+
* @property {string} [parent] The parents' ids, outermost first, joined with "/".
|
|
20
|
+
* @property {string} [id] The record's own id.
|
|
20
21
|
* @property {string | null} [user]
|
|
21
22
|
* @property {boolean} [full] resource_get asked for the whole record.
|
|
23
|
+
* @property {boolean} [merge] resource_update patches (beforeUpdate gets the patch).
|
|
24
|
+
|
|
22
25
|
* @property {Record<string, any>} state Scratch space shared by the hooks of one call.
|
|
23
26
|
*
|
|
24
27
|
* @typedef {object} ResourceHooks
|
|
@@ -29,14 +32,16 @@ import { toolName } from './naming.js';
|
|
|
29
32
|
* @property {(record: any, ctx: HookContext) => any} [afterGet] Returns the record to show.
|
|
30
33
|
* @property {(ctx: HookContext) => string | void | Promise<string | void>} [afterDelete]
|
|
31
34
|
* Returns a note added to the tool's answer.
|
|
35
|
+
* @property {() => any} [describe] What resource_describe adds about the type, under
|
|
36
|
+
* `cli`: the rules the CLI's other hooks enforce, say.
|
|
32
37
|
*/
|
|
33
38
|
|
|
34
|
-
/** Token bodies: expiry as a duration, permission trees checked before sending. */
|
|
39
|
+
/** Token bodies: expiry as a duration (or unix ms), permission trees checked before sending. */
|
|
35
40
|
function tokenBody(body) {
|
|
36
41
|
validatePermissionTree(body.allow, 'allow');
|
|
37
42
|
validatePermissionTree(body.deny, 'deny');
|
|
38
43
|
if (body.expire === undefined) return body;
|
|
39
|
-
const expire =
|
|
44
|
+
const expire = resolveExpiryMs(body.expire);
|
|
40
45
|
const { expire: _expire, ...rest } = body;
|
|
41
46
|
return expire === null ? rest : { ...rest, expire };
|
|
42
47
|
}
|
|
@@ -131,6 +136,15 @@ export function mergeHooks(extra = {}) {
|
|
|
131
136
|
const first = base[stage];
|
|
132
137
|
if (!first) {
|
|
133
138
|
out[stage] = fn;
|
|
139
|
+
} else if (stage === 'describe') {
|
|
140
|
+
// Both are shown, the CLI's last.
|
|
141
|
+
const [one, two] = /** @type {Array<() => any>} */ ([first, fn]);
|
|
142
|
+
out[stage] = async () => {
|
|
143
|
+
const [a, b] = [await one(), await two()];
|
|
144
|
+
return a && b && typeof a === 'object' && typeof b === 'object'
|
|
145
|
+
? { ...a, ...b }
|
|
146
|
+
: (b ?? a);
|
|
147
|
+
};
|
|
134
148
|
} else if (stage === 'afterCreate' || stage === 'afterDelete') {
|
|
135
149
|
// Both notes are kept.
|
|
136
150
|
out[stage] = async (a, b) =>
|
package/lib/mcp/server.js
CHANGED
|
@@ -13,45 +13,50 @@ import { buildTools, listResources, readResource } from './registry.js';
|
|
|
13
13
|
import { withProfile } from './helpers.js';
|
|
14
14
|
import { defineCliDocs } from '../docs.js';
|
|
15
15
|
|
|
16
|
+
/** What a tool declares that the protocol lists. */
|
|
17
|
+
const LISTED = ['name', 'title', 'description', 'inputSchema', 'annotations'];
|
|
18
|
+
|
|
16
19
|
/**
|
|
17
|
-
* Start the MCP server over stdio. Serves the
|
|
18
|
-
* the CLI
|
|
19
|
-
* admin impersonation, where it
|
|
20
|
+
* Start the MCP server over stdio. Serves the eight `resource_*` tools over
|
|
21
|
+
* the catalog, the core's types and the CLI's; every tool takes an
|
|
22
|
+
* optional `profile` (and `user`, for admin impersonation, where it
|
|
23
|
+
* applies).
|
|
20
24
|
*
|
|
21
25
|
* stdout is the protocol channel from here on: diagnostics go to stderr.
|
|
22
26
|
*
|
|
23
27
|
* @param {{
|
|
24
28
|
* version?: string,
|
|
25
|
-
* tools?: Array<object>,
|
|
26
29
|
* instructions?: string,
|
|
27
30
|
* resourceHooks?: Record<string, import('./resource-hooks.js').ResourceHooks>,
|
|
28
31
|
* resourceTypes?: Record<string, import('./tools-resource.js').LocalType>,
|
|
29
32
|
* resourceActions?: Record<string, Record<string, import('./tools-resource.js').LocalAction>>,
|
|
30
33
|
* docs?: Parameters<typeof import('../docs.js').defineCliDocs>[0],
|
|
34
|
+
* readOnly?: boolean,
|
|
31
35
|
* }} [opts]
|
|
32
36
|
* `version` is echoed in the `initialize` response's `serverInfo.version`;
|
|
33
37
|
* callers pass their `package.json#version` so the two never drift.
|
|
34
|
-
* `tools` are CLI-specific tools added to the generic set.
|
|
35
38
|
* `instructions` is the server-level guidance sent to the client.
|
|
36
39
|
* `resourceHooks` are the CLI's hooks on catalog types; `resourceTypes`
|
|
37
40
|
* and `resourceActions` the types and actions it adds to the catalog.
|
|
38
|
-
* `docs` are the guides the CLI ships (see docs.js).
|
|
41
|
+
* `docs` are the guides the CLI ships (see docs.js). `readOnly` serves
|
|
42
|
+
* only what reads: describe, list, get, and the read-only actions.
|
|
39
43
|
*/
|
|
40
44
|
export async function startMCPServer({
|
|
41
45
|
version = 'unknown',
|
|
42
|
-
tools: extra = [],
|
|
43
46
|
instructions,
|
|
44
47
|
resourceHooks,
|
|
45
48
|
resourceTypes,
|
|
46
49
|
resourceActions,
|
|
47
50
|
docs,
|
|
51
|
+
readOnly = false,
|
|
48
52
|
} = {}) {
|
|
49
53
|
const { mcp } = identity();
|
|
50
54
|
defineCliDocs(docs);
|
|
51
|
-
const tools = buildTools(
|
|
55
|
+
const tools = buildTools({
|
|
52
56
|
hooks: resourceHooks,
|
|
53
57
|
types: resourceTypes,
|
|
54
58
|
actions: resourceActions,
|
|
59
|
+
readOnly,
|
|
55
60
|
});
|
|
56
61
|
|
|
57
62
|
const server = new Server(
|
|
@@ -66,9 +71,33 @@ export async function startMCPServer({
|
|
|
66
71
|
);
|
|
67
72
|
|
|
68
73
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
69
|
-
tools: tools.map((
|
|
74
|
+
tools: tools.map((t) =>
|
|
75
|
+
Object.fromEntries(LISTED.filter((k) => t[k] !== undefined).map((k) => [k, t[k]])),
|
|
76
|
+
),
|
|
70
77
|
}));
|
|
71
78
|
|
|
79
|
+
/**
|
|
80
|
+
* Ask the user to confirm an action with a high impact, when the client
|
|
81
|
+
* can show a form (elicitation): the agent cannot answer for them.
|
|
82
|
+
*/
|
|
83
|
+
const elicit = async (message) => {
|
|
84
|
+
const result = await server.elicitInput({
|
|
85
|
+
message,
|
|
86
|
+
requestedSchema: {
|
|
87
|
+
type: 'object',
|
|
88
|
+
properties: {
|
|
89
|
+
confirm: {
|
|
90
|
+
type: 'boolean',
|
|
91
|
+
title: 'Do it',
|
|
92
|
+
description: 'Run it as described above.',
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
required: ['confirm'],
|
|
96
|
+
},
|
|
97
|
+
});
|
|
98
|
+
return result.action === 'accept' && result.content?.confirm === true;
|
|
99
|
+
};
|
|
100
|
+
|
|
72
101
|
server.setRequestHandler(ListResourcesRequestSchema, async () => ({
|
|
73
102
|
resources: await listResources(),
|
|
74
103
|
}));
|
|
@@ -96,15 +125,18 @@ export async function startMCPServer({
|
|
|
96
125
|
const known = tool.inputSchema?.properties;
|
|
97
126
|
const unknown = known ? Object.keys(args).filter((k) => !(k in known)) : [];
|
|
98
127
|
if (unknown.length) {
|
|
99
|
-
|
|
128
|
+
// A filter or body key written at the top: say where it goes.
|
|
129
|
+
const hint = tool.explain?.(args, unknown);
|
|
130
|
+
const message = `Unknown argument${unknown.length > 1 ? 's' : ''} ${unknown.join(', ')}. ${name} takes: ${Object.keys(known).join(', ')}.${hint ? ` ${hint}.` : ''}`;
|
|
100
131
|
return {
|
|
101
132
|
content: [{ type: 'text', text: `Error [input_error]: ${message}` }],
|
|
102
133
|
isError: true,
|
|
103
134
|
_meta: { code: 'input_error', message },
|
|
104
135
|
};
|
|
105
136
|
}
|
|
137
|
+
const extra = server.getClientCapabilities()?.elicitation ? { elicit } : {};
|
|
106
138
|
try {
|
|
107
|
-
return await withProfile(args.profile, () => tool.handler(args));
|
|
139
|
+
return await withProfile(args.profile, () => tool.handler(args, extra));
|
|
108
140
|
} catch (error) {
|
|
109
141
|
// Surface the same {message, code} pair the CLI emits in JSON
|
|
110
142
|
// mode so MCP clients can pattern-match on `[code]` instead of
|