@oh-my-tool/cli 0.3.1 → 0.3.3
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/package.json +2 -2
- package/src/cli/commands/connections.ts +33 -29
- package/src/cli/commands/extension.ts +2 -2
- package/src/cli/commands/run.ts +3 -9
- package/src/cli/commands/search.ts +8 -2
- package/src/cli/context.ts +5 -5
- package/src/cli/index.ts +24 -7
- package/src/cli/output.ts +13 -13
- package/src/config/config.ts +60 -37
- package/src/extension/install.ts +116 -5
- package/src/extension/loader.ts +1 -1
- package/src/extension/manifest.ts +25 -1
- package/src/integration/manager.ts +4 -4
- package/src/policy/policy.ts +2 -0
- package/src/runtime/provider.ts +17 -0
- package/src/runtime/providers/mcp/oauth-provider.ts +22 -1
- package/src/runtime/providers/mcp/oauth-store.ts +12 -1
- package/src/runtime/providers/mcp/provider.ts +2 -0
- package/src/runtime/providers/native/provider.ts +29 -9
- package/src/runtime/result.ts +12 -5
- package/src/runtime/runtime.ts +79 -25
- package/src/runtime/tool-registry.ts +23 -8
- package/src/version.ts +1 -1
- package/src/core/executor.ts +0 -99
- package/src/core/registry.ts +0 -33
- package/src/core/result.ts +0 -14
- package/src/core/schema.ts +0 -2
- package/src/search/search.ts +0 -78
|
@@ -20,15 +20,26 @@ export function parseManifest(raw: string): ExtensionManifest {
|
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
export function validateManifest(manifest: ExtensionManifest): void {
|
|
23
|
+
if (!/^[a-z0-9][a-z0-9_-]*$/.test(manifest.id)) {
|
|
24
|
+
throw new ManifestError("manifest 'id' must contain only lowercase letters, numbers, underscores, and hyphens");
|
|
25
|
+
}
|
|
23
26
|
if (!manifest.name || typeof manifest.name !== "string") {
|
|
24
27
|
throw new ManifestError("manifest must contain a string 'name'");
|
|
25
28
|
}
|
|
26
29
|
if (!manifest.version || typeof manifest.version !== "string") {
|
|
27
30
|
throw new ManifestError("manifest must contain a string 'version'");
|
|
28
31
|
}
|
|
32
|
+
if (!FULL_VERSION_RE.test(manifest.version)) {
|
|
33
|
+
throw new ManifestError(`invalid manifest version '${manifest.version}'`);
|
|
34
|
+
}
|
|
29
35
|
if (!manifest.sdkVersion || typeof manifest.sdkVersion !== "string") {
|
|
30
36
|
throw new ManifestError("manifest must contain a string 'sdkVersion'");
|
|
31
37
|
}
|
|
38
|
+
if (manifest.connectionSchema !== undefined && (
|
|
39
|
+
manifest.connectionSchema === null || typeof manifest.connectionSchema !== "object" || Array.isArray(manifest.connectionSchema)
|
|
40
|
+
)) {
|
|
41
|
+
throw new ManifestError("manifest 'connectionSchema' must be an object");
|
|
42
|
+
}
|
|
32
43
|
|
|
33
44
|
const seen = new Set<string>();
|
|
34
45
|
for (const tool of manifest.tools) {
|
|
@@ -47,6 +58,19 @@ export function validateManifest(manifest: ExtensionManifest): void {
|
|
|
47
58
|
);
|
|
48
59
|
}
|
|
49
60
|
}
|
|
61
|
+
|
|
62
|
+
if (manifest.connectionCheckTool !== undefined) {
|
|
63
|
+
if (typeof manifest.connectionCheckTool !== "string" || manifest.connectionCheckTool.length === 0) {
|
|
64
|
+
throw new ManifestError("manifest 'connectionCheckTool' must be a non-empty string");
|
|
65
|
+
}
|
|
66
|
+
const checkTool = manifest.tools.find((tool) => tool.name === manifest.connectionCheckTool);
|
|
67
|
+
if (!checkTool || !manifest.connectionCheckTool.startsWith(`${manifest.id}.`)) {
|
|
68
|
+
throw new ManifestError("manifest 'connectionCheckTool' must name a declared tool prefixed by the extension id");
|
|
69
|
+
}
|
|
70
|
+
if ((checkTool.risk ?? "read") !== "read") {
|
|
71
|
+
throw new ManifestError("manifest 'connectionCheckTool' must be read-only");
|
|
72
|
+
}
|
|
73
|
+
}
|
|
50
74
|
}
|
|
51
75
|
|
|
52
76
|
const FULL_VERSION_RE = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/;
|
|
@@ -112,4 +136,4 @@ export function validateHandlers(
|
|
|
112
136
|
);
|
|
113
137
|
}
|
|
114
138
|
}
|
|
115
|
-
}
|
|
139
|
+
}
|
|
@@ -274,17 +274,17 @@ export function createIntegrationManager(options: IntegrationManagerOptions) {
|
|
|
274
274
|
const recorded = records[agent.id];
|
|
275
275
|
if (!recorded) {
|
|
276
276
|
if (!pathPresent(agent.target)) {
|
|
277
|
-
return result(agent, "not-installed", "not installed; run `
|
|
277
|
+
return result(agent, "not-installed", "not installed; run `ohmytool integrate` to install");
|
|
278
278
|
}
|
|
279
279
|
return result(agent, "conflict", "target exists but is not managed by OMT");
|
|
280
280
|
}
|
|
281
281
|
try {
|
|
282
282
|
assertSafeManagedState(agent, recorded, options.omtHome, platform);
|
|
283
283
|
} catch {
|
|
284
|
-
return result(agent, "conflict", "recorded state is unsafe; run `
|
|
284
|
+
return result(agent, "conflict", "recorded state is unsafe; run `ohmytool integrate repair`");
|
|
285
285
|
}
|
|
286
286
|
if (!pathPresent(agent.target) || !safeRealpath(agent.target)) {
|
|
287
|
-
return result(agent, "broken", "link is missing; run `
|
|
287
|
+
return result(agent, "broken", "link is missing; run `ohmytool integrate repair`");
|
|
288
288
|
}
|
|
289
289
|
if (!isManagedLink(agent.target, recorded.canonical)) {
|
|
290
290
|
return result(agent, "conflict", "target was replaced by unmanaged content");
|
|
@@ -292,7 +292,7 @@ export function createIntegrationManager(options: IntegrationManagerOptions) {
|
|
|
292
292
|
if (recorded.version === options.skillVersion) {
|
|
293
293
|
return result(agent, "current");
|
|
294
294
|
}
|
|
295
|
-
return result(agent, "update-available", `new version ${options.skillVersion} available; run \`
|
|
295
|
+
return result(agent, "update-available", `new version ${options.skillVersion} available; run \`ohmytool integrate\``);
|
|
296
296
|
});
|
|
297
297
|
}
|
|
298
298
|
|
package/src/policy/policy.ts
CHANGED
package/src/runtime/provider.ts
CHANGED
|
@@ -11,6 +11,22 @@ export interface ToolDescriptor {
|
|
|
11
11
|
source: { id: string; kind: string; version?: string };
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
+
export interface ToolSearchOptions {
|
|
15
|
+
readonly limit?: number;
|
|
16
|
+
readonly provider?: string;
|
|
17
|
+
readonly source?: string;
|
|
18
|
+
readonly risk?: ToolDescriptor["risk"];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface ProviderStatus {
|
|
22
|
+
readonly id: string;
|
|
23
|
+
readonly kind: string;
|
|
24
|
+
readonly status: "available" | "unavailable";
|
|
25
|
+
readonly code?: string;
|
|
26
|
+
readonly message?: string;
|
|
27
|
+
readonly namespace?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
14
30
|
export type ToolSearchResult = Omit<ToolDescriptor, "inputSchema">;
|
|
15
31
|
|
|
16
32
|
export interface ExecutionContext {
|
|
@@ -22,6 +38,7 @@ export interface ExecutionContext {
|
|
|
22
38
|
export interface ToolProvider {
|
|
23
39
|
readonly id: string;
|
|
24
40
|
readonly kind: string;
|
|
41
|
+
readonly namespace?: string;
|
|
25
42
|
listTools(): Promise<readonly ToolDescriptor[]>;
|
|
26
43
|
execute(toolId: string, input: unknown, context: ExecutionContext): Promise<ToolResult>;
|
|
27
44
|
close?(): Promise<void>;
|
|
@@ -33,11 +33,13 @@ import {
|
|
|
33
33
|
export interface McpOAuthProviderOptions {
|
|
34
34
|
readonly redirectUrl?: URL;
|
|
35
35
|
readonly interactive?: boolean;
|
|
36
|
+
readonly forceDynamicRegistration?: boolean;
|
|
36
37
|
}
|
|
37
38
|
|
|
38
39
|
export interface McpOAuthClientProvider extends OAuthClientProvider {
|
|
39
40
|
readonly redirectUrl: URL;
|
|
40
41
|
readonly secretValues: readonly string[];
|
|
42
|
+
readonly clientConfigurationFingerprint: string;
|
|
41
43
|
authorizationUrl(): URL | undefined;
|
|
42
44
|
authorizationState(): string | undefined;
|
|
43
45
|
clearVerifier(): Promise<void>;
|
|
@@ -65,6 +67,7 @@ function oauthAuthRequired(serverId: string): RuntimeError {
|
|
|
65
67
|
|
|
66
68
|
class PersistentMcpOAuthProvider implements McpOAuthClientProvider {
|
|
67
69
|
readonly clientMetadata: OAuthClientMetadata;
|
|
70
|
+
readonly clientConfigurationFingerprint: string;
|
|
68
71
|
private pendingAuthorizationUrl: URL | undefined;
|
|
69
72
|
private pendingState: string | undefined;
|
|
70
73
|
|
|
@@ -85,6 +88,12 @@ class PersistentMcpOAuthProvider implements McpOAuthClientProvider {
|
|
|
85
88
|
client_name: "Oh My Tool",
|
|
86
89
|
...(config.auth.scopes.length === 0 ? {} : { scope: config.auth.scopes.join(" ") }),
|
|
87
90
|
};
|
|
91
|
+
this.clientConfigurationFingerprint = JSON.stringify({
|
|
92
|
+
redirectUrl: redirectUrl.toString(),
|
|
93
|
+
clientId: config.auth.clientId ?? null,
|
|
94
|
+
clientSecretConfigured: config.auth.clientSecretSecret !== undefined,
|
|
95
|
+
metadata: this.clientMetadata,
|
|
96
|
+
});
|
|
88
97
|
}
|
|
89
98
|
|
|
90
99
|
state(): string {
|
|
@@ -172,6 +181,9 @@ export async function createMcpOAuthProvider(
|
|
|
172
181
|
const interactive = options.interactive ?? false;
|
|
173
182
|
const store = createMcpOAuthStore(serverId, secrets);
|
|
174
183
|
if (!interactive && await store.tokens() === undefined) throw oauthAuthRequired(serverId);
|
|
184
|
+
if (options.forceDynamicRegistration === true && config.auth.clientId === undefined) {
|
|
185
|
+
await store.clear("client");
|
|
186
|
+
}
|
|
175
187
|
let preRegisteredClient: StoredOAuthClientInformation | undefined;
|
|
176
188
|
const secretValues: string[] = [];
|
|
177
189
|
if (config.auth.clientId !== undefined) {
|
|
@@ -191,7 +203,7 @@ export async function createMcpOAuthProvider(
|
|
|
191
203
|
client_secret: clientSecret,
|
|
192
204
|
};
|
|
193
205
|
}
|
|
194
|
-
|
|
206
|
+
const provider = new PersistentMcpOAuthProvider(
|
|
195
207
|
serverId,
|
|
196
208
|
config,
|
|
197
209
|
store,
|
|
@@ -200,6 +212,14 @@ export async function createMcpOAuthProvider(
|
|
|
200
212
|
secretValues,
|
|
201
213
|
interactive,
|
|
202
214
|
);
|
|
215
|
+
const previousFingerprint = await store.clientConfiguration();
|
|
216
|
+
if (previousFingerprint !== provider.clientConfigurationFingerprint) {
|
|
217
|
+
if (previousFingerprint !== undefined) {
|
|
218
|
+
await store.clear("client");
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
await store.saveClientConfiguration(provider.clientConfigurationFingerprint);
|
|
222
|
+
return provider;
|
|
203
223
|
}
|
|
204
224
|
|
|
205
225
|
function oauthConfig(serverId: string, config: McpHttpServerConfig): OAuthMcpServerConfig {
|
|
@@ -287,6 +307,7 @@ export async function authorizeMcpServer(
|
|
|
287
307
|
const activeProvider = await createMcpOAuthProvider(serverId, validated, secrets, {
|
|
288
308
|
redirectUrl: callback.redirectUrl,
|
|
289
309
|
interactive: true,
|
|
310
|
+
forceDynamicRegistration: true,
|
|
290
311
|
});
|
|
291
312
|
provider = activeProvider;
|
|
292
313
|
firstClient = createClient({ name: "oh-my-tool", version: VERSION });
|
|
@@ -11,6 +11,8 @@ export interface McpOAuthStore {
|
|
|
11
11
|
saveTokens(tokens: StoredOAuthTokens): Promise<void>;
|
|
12
12
|
clientInformation(): Promise<StoredOAuthClientInformation | undefined>;
|
|
13
13
|
saveClientInformation(info: StoredOAuthClientInformation): Promise<void>;
|
|
14
|
+
clientConfiguration(): Promise<string | undefined>;
|
|
15
|
+
saveClientConfiguration(fingerprint: string): Promise<void>;
|
|
14
16
|
codeVerifier(): Promise<string | undefined>;
|
|
15
17
|
saveCodeVerifier(value: string): Promise<void>;
|
|
16
18
|
discoveryState(): Promise<OAuthDiscoveryState | undefined>;
|
|
@@ -19,13 +21,14 @@ export interface McpOAuthStore {
|
|
|
19
21
|
clearAll(): Promise<void>;
|
|
20
22
|
}
|
|
21
23
|
|
|
22
|
-
type CredentialScope = "tokens" | "client" | "verifier" | "discovery";
|
|
24
|
+
type CredentialScope = "tokens" | "client" | "client-config" | "verifier" | "discovery";
|
|
23
25
|
|
|
24
26
|
function credentialNames(serverId: string): Record<CredentialScope, string> {
|
|
25
27
|
const prefix = `mcp:${serverId}:oauth`;
|
|
26
28
|
return {
|
|
27
29
|
tokens: `${prefix}:tokens`,
|
|
28
30
|
client: `${prefix}:client`,
|
|
31
|
+
"client-config": `${prefix}:client-config`,
|
|
29
32
|
verifier: `${prefix}:verifier`,
|
|
30
33
|
discovery: `${prefix}:discovery`,
|
|
31
34
|
};
|
|
@@ -72,6 +75,14 @@ export class SecretMcpOAuthStore implements McpOAuthStore {
|
|
|
72
75
|
return this.secrets.set(this.names.client, JSON.stringify(info));
|
|
73
76
|
}
|
|
74
77
|
|
|
78
|
+
clientConfiguration(): Promise<string | undefined> {
|
|
79
|
+
return this.secrets.get(this.names["client-config"]);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
saveClientConfiguration(fingerprint: string): Promise<void> {
|
|
83
|
+
return this.secrets.set(this.names["client-config"], fingerprint);
|
|
84
|
+
}
|
|
85
|
+
|
|
75
86
|
codeVerifier(): Promise<string | undefined> {
|
|
76
87
|
return this.secrets.get(this.names.verifier);
|
|
77
88
|
}
|
|
@@ -17,6 +17,7 @@ export interface McpProviderOptions {
|
|
|
17
17
|
export class McpProvider implements ToolProvider {
|
|
18
18
|
readonly id: string;
|
|
19
19
|
readonly kind = "mcp";
|
|
20
|
+
readonly namespace: string;
|
|
20
21
|
private session?: McpSession;
|
|
21
22
|
private descriptors?: readonly ToolDescriptor[];
|
|
22
23
|
private readonly routes = new Map<string, string>();
|
|
@@ -24,6 +25,7 @@ export class McpProvider implements ToolProvider {
|
|
|
24
25
|
|
|
25
26
|
constructor(private readonly options: McpProviderOptions) {
|
|
26
27
|
this.id = `mcp:${options.serverId}`;
|
|
28
|
+
this.namespace = options.config.namespace;
|
|
27
29
|
}
|
|
28
30
|
|
|
29
31
|
async listTools(): Promise<readonly ToolDescriptor[]> {
|
|
@@ -8,7 +8,15 @@ export class NativeExtensionProvider implements ToolProvider {
|
|
|
8
8
|
readonly id = "native";
|
|
9
9
|
readonly kind = "native";
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
private readonly discover: (home: string) => InstalledExtension[];
|
|
12
|
+
private snapshot?: { extensions: InstalledExtension[]; routes: Map<string, InstalledExtension> };
|
|
13
|
+
|
|
14
|
+
constructor(
|
|
15
|
+
private readonly homeOrPaths: string | Pick<OhMyToolPaths, "home">,
|
|
16
|
+
discover: (home: string) => InstalledExtension[] = discoverExtensions,
|
|
17
|
+
) {
|
|
18
|
+
this.discover = discover;
|
|
19
|
+
}
|
|
12
20
|
|
|
13
21
|
private home(): string {
|
|
14
22
|
return typeof this.homeOrPaths === "string" ? this.homeOrPaths : this.homeOrPaths.home;
|
|
@@ -16,7 +24,7 @@ export class NativeExtensionProvider implements ToolProvider {
|
|
|
16
24
|
|
|
17
25
|
async listTools(): Promise<readonly ToolDescriptor[]> {
|
|
18
26
|
const descriptors: ToolDescriptor[] = [];
|
|
19
|
-
for (const extension of
|
|
27
|
+
for (const extension of this.getSnapshot().extensions) {
|
|
20
28
|
for (const tool of extension.manifest.tools) {
|
|
21
29
|
descriptors.push({
|
|
22
30
|
id: tool.name,
|
|
@@ -40,18 +48,30 @@ export class NativeExtensionProvider implements ToolProvider {
|
|
|
40
48
|
}
|
|
41
49
|
|
|
42
50
|
async execute(toolId: string, input: unknown, context: ExecutionContext): Promise<ToolResult> {
|
|
43
|
-
const extension = this.
|
|
51
|
+
const extension = this.getSnapshot().routes.get(toolId);
|
|
52
|
+
if (!extension) throw new Error(`unknown native tool '${toolId}'`);
|
|
44
53
|
const definition = await loadExtension(extension);
|
|
45
54
|
const handler = definition.handlers[toolId];
|
|
46
55
|
if (!handler) throw new Error(`no handler for ${toolId}`);
|
|
47
56
|
return handler({ toolName: toolId, logger: context.logger, config: context.config, secrets: context.secrets }, input);
|
|
48
57
|
}
|
|
49
58
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
return
|
|
59
|
+
async hasTool(toolId: string): Promise<boolean> {
|
|
60
|
+
return this.getSnapshot().routes.has(toolId);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
installedExtensions(): readonly InstalledExtension[] {
|
|
64
|
+
return this.getSnapshot().extensions;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
private getSnapshot(): { extensions: InstalledExtension[]; routes: Map<string, InstalledExtension> } {
|
|
68
|
+
if (this.snapshot) return this.snapshot;
|
|
69
|
+
const extensions = this.discover(this.home());
|
|
70
|
+
const routes = new Map<string, InstalledExtension>();
|
|
71
|
+
for (const extension of extensions) {
|
|
72
|
+
for (const tool of extension.manifest.tools) routes.set(tool.name, extension);
|
|
73
|
+
}
|
|
74
|
+
this.snapshot = { extensions, routes };
|
|
75
|
+
return this.snapshot;
|
|
56
76
|
}
|
|
57
77
|
}
|
package/src/runtime/result.ts
CHANGED
|
@@ -3,10 +3,17 @@ export interface ToolResult {
|
|
|
3
3
|
meta?: Record<string, unknown>;
|
|
4
4
|
}
|
|
5
5
|
|
|
6
|
-
export interface
|
|
7
|
-
ok:
|
|
6
|
+
export interface ExecutionOk {
|
|
7
|
+
ok: true;
|
|
8
8
|
toolId: string;
|
|
9
|
-
output
|
|
10
|
-
meta
|
|
11
|
-
error?: { code: string; message: string; details?: unknown };
|
|
9
|
+
output: unknown;
|
|
10
|
+
meta: Record<string, unknown>;
|
|
12
11
|
}
|
|
12
|
+
|
|
13
|
+
export interface ExecutionError {
|
|
14
|
+
ok: false;
|
|
15
|
+
toolId: string;
|
|
16
|
+
error: { code: string; message: string; details?: unknown };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export type ExecutionResult = ExecutionOk | ExecutionError;
|
package/src/runtime/runtime.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ExecutionContext, ToolDescriptor, ToolProvider, ToolSearchResult } from "./provider";
|
|
1
|
+
import type { ExecutionContext, ProviderStatus, ToolDescriptor, ToolProvider, ToolSearchOptions, ToolSearchResult } from "./provider";
|
|
2
2
|
import type { ExecutionResult } from "./result";
|
|
3
3
|
import { executeRuntimeTool, type CreateExecutionContext, type PolicyPreflight } from "./executor";
|
|
4
4
|
import { RuntimeError } from "./errors";
|
|
@@ -20,23 +20,30 @@ interface RuntimeState {
|
|
|
20
20
|
|
|
21
21
|
export class ToolRuntime {
|
|
22
22
|
private closePromise?: Promise<void>;
|
|
23
|
+
private readonly discovery = new Map<string, Promise<void>>();
|
|
24
|
+
private readonly statuses = new Map<string, ProviderStatus>();
|
|
25
|
+
private readonly closedProviders = new Set<string>();
|
|
23
26
|
|
|
24
27
|
constructor(private readonly state: RuntimeState, private readonly registeredProviders: readonly ToolProvider[] = []) {}
|
|
25
28
|
|
|
26
|
-
search(query: string): Promise<ToolSearchResult[]> {
|
|
27
|
-
|
|
29
|
+
async search(query: string, options?: ToolSearchOptions): Promise<ToolSearchResult[]> {
|
|
30
|
+
await this.discoverAll();
|
|
31
|
+
return this.state.tools.search(query, options);
|
|
28
32
|
}
|
|
29
33
|
|
|
30
|
-
describe(toolId: string): Promise<ToolDescriptor> {
|
|
34
|
+
async describe(toolId: string): Promise<ToolDescriptor> {
|
|
35
|
+
await this.discoverForTarget(toolId);
|
|
31
36
|
const descriptor = this.state.tools.get(toolId);
|
|
32
|
-
if (!descriptor)
|
|
33
|
-
return
|
|
37
|
+
if (!descriptor) throw this.targetError(toolId);
|
|
38
|
+
return descriptor;
|
|
34
39
|
}
|
|
35
40
|
|
|
36
41
|
async run(toolId: string, input: unknown): Promise<ExecutionResult> {
|
|
42
|
+
await this.discoverForTarget(toolId);
|
|
37
43
|
const descriptor = this.state.tools.get(toolId);
|
|
38
44
|
if (!descriptor) {
|
|
39
|
-
|
|
45
|
+
const error = this.targetError(toolId);
|
|
46
|
+
return { ok: false, toolId, error: { code: error.code, message: error.message } };
|
|
40
47
|
}
|
|
41
48
|
const provider = this.state.providers.require(descriptor.provider.id);
|
|
42
49
|
return executeRuntimeTool({
|
|
@@ -47,32 +54,45 @@ export class ToolRuntime {
|
|
|
47
54
|
}, (input ?? {}) as Record<string, unknown>);
|
|
48
55
|
}
|
|
49
56
|
|
|
57
|
+
providerStatuses(): readonly ProviderStatus[] {
|
|
58
|
+
return [...this.statuses.values()].sort((a, b) => a.id.localeCompare(b.id));
|
|
59
|
+
}
|
|
60
|
+
|
|
50
61
|
close(): Promise<void> {
|
|
51
62
|
if (this.closePromise) return this.closePromise;
|
|
52
63
|
this.closePromise = (async () => {
|
|
53
64
|
const errors: unknown[] = [];
|
|
54
65
|
for (const provider of [...this.registeredProviders].reverse()) {
|
|
55
|
-
|
|
56
|
-
try { await provider.close(); } catch (error) { errors.push(error); }
|
|
66
|
+
try { await this.closeProvider(provider); } catch (error) { errors.push(error); }
|
|
57
67
|
}
|
|
58
68
|
if (errors.length > 0) throw errors[0];
|
|
59
69
|
})();
|
|
60
70
|
return this.closePromise;
|
|
61
71
|
}
|
|
62
|
-
}
|
|
63
72
|
|
|
64
|
-
async
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
}));
|
|
68
|
-
}
|
|
73
|
+
private async discoverAll(): Promise<void> {
|
|
74
|
+
await Promise.all(this.registeredProviders.map((provider) => this.discoverProvider(provider)));
|
|
75
|
+
}
|
|
69
76
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
77
|
+
private async discoverForTarget(toolId: string): Promise<void> {
|
|
78
|
+
const nativeProviders = this.registeredProviders.filter((provider) => provider.kind === "native");
|
|
79
|
+
await Promise.all(nativeProviders.map((provider) => this.discoverProvider(provider)));
|
|
80
|
+
if (this.state.tools.get(toolId)) return;
|
|
81
|
+
await Promise.all(this.registeredProviders
|
|
82
|
+
.filter((provider) => provider.kind !== "native")
|
|
83
|
+
.map((provider) => this.discoverProvider(provider)));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
private discoverProvider(provider: ToolProvider): Promise<void> {
|
|
87
|
+
const current = this.discovery.get(provider.id);
|
|
88
|
+
if (current) return current;
|
|
89
|
+
const promise = this.discoverProviderOnce(provider);
|
|
90
|
+
this.discovery.set(provider.id, promise);
|
|
91
|
+
return promise;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
private async discoverProviderOnce(provider: ToolProvider): Promise<void> {
|
|
95
|
+
try {
|
|
76
96
|
const descriptors = await provider.listTools();
|
|
77
97
|
for (const descriptor of descriptors) {
|
|
78
98
|
if (descriptor.provider.id !== provider.id || descriptor.provider.kind !== provider.kind) {
|
|
@@ -82,12 +102,46 @@ export async function createToolRuntime(options: ToolRuntimeOptions): Promise<To
|
|
|
82
102
|
);
|
|
83
103
|
}
|
|
84
104
|
}
|
|
85
|
-
tools.register(descriptors);
|
|
105
|
+
this.state.tools.register(descriptors);
|
|
106
|
+
this.statuses.set(provider.id, {
|
|
107
|
+
id: provider.id,
|
|
108
|
+
kind: provider.kind,
|
|
109
|
+
status: "available",
|
|
110
|
+
...("namespace" in provider && typeof provider.namespace === "string" ? { namespace: provider.namespace } : {}),
|
|
111
|
+
});
|
|
112
|
+
} catch (error) {
|
|
113
|
+
if (provider.kind !== "mcp") throw error;
|
|
114
|
+
const typed = error as { code?: unknown; message?: unknown };
|
|
115
|
+
this.statuses.set(provider.id, {
|
|
116
|
+
id: provider.id,
|
|
117
|
+
kind: provider.kind,
|
|
118
|
+
status: "unavailable",
|
|
119
|
+
...("namespace" in provider && typeof provider.namespace === "string" ? { namespace: provider.namespace } : {}),
|
|
120
|
+
code: typeof typed.code === "string" ? typed.code : "PROVIDER_UNAVAILABLE",
|
|
121
|
+
message: typeof typed.message === "string" ? typed.message : "provider discovery failed",
|
|
122
|
+
});
|
|
123
|
+
try { await this.closeProvider(provider); } catch { /* preserve discovery status */ }
|
|
86
124
|
}
|
|
87
|
-
} catch (error) {
|
|
88
|
-
await closeProviders(options.providers);
|
|
89
|
-
throw error;
|
|
90
125
|
}
|
|
126
|
+
|
|
127
|
+
private targetError(toolId: string): RuntimeError {
|
|
128
|
+
const unavailable = this.providerStatuses().find((status) =>
|
|
129
|
+
status.status === "unavailable" && status.namespace !== undefined && toolId.startsWith(`${status.namespace}.`));
|
|
130
|
+
if (unavailable) return new RuntimeError("PROVIDER_UNAVAILABLE", `provider '${unavailable.id}' is unavailable`);
|
|
131
|
+
return new RuntimeError("TOOL_NOT_FOUND", `unknown tool '${toolId}'`);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
private async closeProvider(provider: ToolProvider): Promise<void> {
|
|
135
|
+
if (!provider.close || this.closedProviders.has(provider.id)) return;
|
|
136
|
+
this.closedProviders.add(provider.id);
|
|
137
|
+
await provider.close();
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export async function createToolRuntime(options: ToolRuntimeOptions): Promise<ToolRuntime> {
|
|
142
|
+
const providers = new ProviderRegistry();
|
|
143
|
+
const tools = new ToolRegistry();
|
|
144
|
+
for (const provider of options.providers) providers.register(provider);
|
|
91
145
|
return new ToolRuntime({
|
|
92
146
|
providers,
|
|
93
147
|
tools,
|
|
@@ -1,9 +1,12 @@
|
|
|
1
|
-
import type { ToolDescriptor, ToolSearchResult } from "./provider";
|
|
1
|
+
import type { ToolDescriptor, ToolSearchOptions, ToolSearchResult } from "./provider";
|
|
2
2
|
import { RuntimeError } from "./errors";
|
|
3
3
|
|
|
4
4
|
const NAME_WEIGHT = 3;
|
|
5
5
|
const KEYWORD_WEIGHT = 2;
|
|
6
6
|
const DESCRIPTION_WEIGHT = 1;
|
|
7
|
+
const EXACT_BOOST = 4;
|
|
8
|
+
const PREFIX_BOOST = 2;
|
|
9
|
+
const MAX_SEARCH_RESULTS = 100;
|
|
7
10
|
|
|
8
11
|
export class ToolRegistry {
|
|
9
12
|
private readonly tools = new Map<string, ToolDescriptor>();
|
|
@@ -25,13 +28,17 @@ export class ToolRegistry {
|
|
|
25
28
|
return this.tools.get(toolId);
|
|
26
29
|
}
|
|
27
30
|
|
|
28
|
-
search(query: string): ToolSearchResult[] {
|
|
31
|
+
search(query: string, options: ToolSearchOptions = {}): ToolSearchResult[] {
|
|
29
32
|
const tokens = query.toLowerCase().split(/\s+/).filter(Boolean);
|
|
30
33
|
if (tokens.length === 0) return [];
|
|
31
34
|
return [...this.tools.values()]
|
|
35
|
+
.filter((descriptor) => options.provider === undefined || descriptor.provider.id === options.provider)
|
|
36
|
+
.filter((descriptor) => options.source === undefined || descriptor.source.id === options.source)
|
|
37
|
+
.filter((descriptor) => options.risk === undefined || descriptor.risk === options.risk)
|
|
32
38
|
.map((descriptor) => ({ descriptor, score: this.score(descriptor, tokens) }))
|
|
33
39
|
.filter((hit) => hit.score > 0)
|
|
34
|
-
.sort((a, b) => b.score - a.score)
|
|
40
|
+
.sort((a, b) => b.score - a.score || a.descriptor.id.localeCompare(b.descriptor.id))
|
|
41
|
+
.slice(0, Math.min(Math.max(Math.trunc(options.limit ?? MAX_SEARCH_RESULTS), 1), MAX_SEARCH_RESULTS))
|
|
35
42
|
.map(({ descriptor }) => {
|
|
36
43
|
const { inputSchema: _inputSchema, ...summary } = descriptor;
|
|
37
44
|
return summary;
|
|
@@ -43,12 +50,20 @@ export class ToolRegistry {
|
|
|
43
50
|
const description = descriptor.description.toLowerCase();
|
|
44
51
|
const keywords = (descriptor.keywords ?? []).map((keyword) => keyword.toLowerCase());
|
|
45
52
|
return tokens.reduce((score, token) => {
|
|
46
|
-
|
|
47
|
-
if (
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
53
|
+
const idScore = matchWeight(id, token, NAME_WEIGHT);
|
|
54
|
+
if (idScore > 0) return score + idScore;
|
|
55
|
+
const keywordScore = Math.max(0, ...keywords.map((keyword) => matchWeight(keyword, token, KEYWORD_WEIGHT)));
|
|
56
|
+
if (keywordScore > 0) return score + keywordScore;
|
|
57
|
+
const descriptionScore = matchWeight(description, token, DESCRIPTION_WEIGHT);
|
|
58
|
+
if (descriptionScore > 0) return score + descriptionScore;
|
|
51
59
|
return score;
|
|
52
60
|
}, 0);
|
|
53
61
|
}
|
|
54
62
|
}
|
|
63
|
+
|
|
64
|
+
function matchWeight(value: string, token: string, base: number): number {
|
|
65
|
+
if (value === token) return base + EXACT_BOOST;
|
|
66
|
+
if (value.startsWith(token)) return base + PREFIX_BOOST;
|
|
67
|
+
if (value.includes(token) || token.includes(value)) return base;
|
|
68
|
+
return 0;
|
|
69
|
+
}
|
package/src/version.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const VERSION = "0.3.
|
|
1
|
+
export const VERSION = "0.3.3";
|
package/src/core/executor.ts
DELETED
|
@@ -1,99 +0,0 @@
|
|
|
1
|
-
import type { Logger, SecretStore, ToolContext, ToolResult } from "@oh-my-tool/sdk";
|
|
2
|
-
import { ToolError } from "@oh-my-tool/sdk";
|
|
3
|
-
import type { Config } from "../config/config";
|
|
4
|
-
import { getConnectionConfig, sanitizeExtensionConnections } from "../config/config";
|
|
5
|
-
import { resolveTool, OmtError, type Registry } from "./registry";
|
|
6
|
-
import { validateInput, type Schema } from "./schema";
|
|
7
|
-
import { validateConnectionInput, applyLimits, PolicyError } from "../policy/policy";
|
|
8
|
-
import { loadExtension } from "../extension/loader";
|
|
9
|
-
import type { OmtResult } from "./result";
|
|
10
|
-
|
|
11
|
-
const noopLogger: Logger = {
|
|
12
|
-
debug: () => {},
|
|
13
|
-
info: () => {},
|
|
14
|
-
warn: () => {},
|
|
15
|
-
error: () => {},
|
|
16
|
-
};
|
|
17
|
-
|
|
18
|
-
export interface ExecutorDeps {
|
|
19
|
-
registry: Registry;
|
|
20
|
-
config: Config;
|
|
21
|
-
secrets: SecretStore;
|
|
22
|
-
logger?: Logger;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
function hasConnection(schema: Schema | undefined): boolean {
|
|
26
|
-
return Boolean(schema?.properties && "connection" in schema.properties);
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export async function executeTool(
|
|
30
|
-
deps: ExecutorDeps,
|
|
31
|
-
toolName: string,
|
|
32
|
-
rawInput: Record<string, unknown>,
|
|
33
|
-
): Promise<OmtResult> {
|
|
34
|
-
const started = Date.now();
|
|
35
|
-
try {
|
|
36
|
-
const { extension, tool } = resolveTool(deps.registry, toolName);
|
|
37
|
-
const schema = tool.inputSchema as Schema | undefined;
|
|
38
|
-
|
|
39
|
-
const limits = applyLimits(rawInput);
|
|
40
|
-
const normalized = { ...rawInput, maxRows: limits.maxRows, timeoutMs: limits.timeoutMs };
|
|
41
|
-
|
|
42
|
-
const needsConnection = hasConnection(schema) || "connection" in normalized;
|
|
43
|
-
if (needsConnection) {
|
|
44
|
-
validateConnectionInput(normalized, deps.config, extension.id);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
const input = validateInput(schema, normalized);
|
|
48
|
-
|
|
49
|
-
const connectionCfg = needsConnection
|
|
50
|
-
? getConnectionConfig(deps.config, extension.id, String(input.connection))
|
|
51
|
-
: undefined;
|
|
52
|
-
|
|
53
|
-
const ctx: ToolContext = {
|
|
54
|
-
toolName,
|
|
55
|
-
logger: deps.logger ?? noopLogger,
|
|
56
|
-
config: (connectionCfg ?? { connections: sanitizeExtensionConnections(deps.config)[extension.id] ?? {} }) as Record<string, unknown>,
|
|
57
|
-
secrets: deps.secrets,
|
|
58
|
-
};
|
|
59
|
-
|
|
60
|
-
const def = await loadExtension(extension);
|
|
61
|
-
const handler = def.handlers[toolName];
|
|
62
|
-
if (!handler) {
|
|
63
|
-
throw new OmtError("HANDLER_MISSING", `no handler for ${toolName}`);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
const result: ToolResult = await handler(ctx, input);
|
|
67
|
-
const durationMs = Date.now() - started;
|
|
68
|
-
return {
|
|
69
|
-
ok: true,
|
|
70
|
-
tool: toolName,
|
|
71
|
-
data: result.data,
|
|
72
|
-
meta: { durationMs, ...(result.meta ?? {}) },
|
|
73
|
-
};
|
|
74
|
-
} catch (e) {
|
|
75
|
-
const durationMs = Date.now() - started;
|
|
76
|
-
if (e instanceof PolicyError) {
|
|
77
|
-
return { ok: false, tool: toolName, error: { code: "POLICY_VIOLATION", message: e.message } };
|
|
78
|
-
}
|
|
79
|
-
if (e instanceof ToolError) {
|
|
80
|
-
return { ok: false, tool: toolName, error: { code: e.code, message: e.message } };
|
|
81
|
-
}
|
|
82
|
-
if (e instanceof OmtError) {
|
|
83
|
-
return { ok: false, tool: toolName, error: { code: e.code, message: e.message } };
|
|
84
|
-
}
|
|
85
|
-
if (e && typeof e === "object" && "code" in e && typeof (e as { code?: unknown }).code === "string") {
|
|
86
|
-
return {
|
|
87
|
-
ok: false,
|
|
88
|
-
tool: toolName,
|
|
89
|
-
error: { code: (e as { code: string }).code, message: e instanceof Error ? e.message : String(e) },
|
|
90
|
-
};
|
|
91
|
-
}
|
|
92
|
-
return {
|
|
93
|
-
ok: false,
|
|
94
|
-
tool: toolName,
|
|
95
|
-
error: { code: "EXECUTION_FAILED", message: e instanceof Error ? e.message : String(e) },
|
|
96
|
-
};
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
|