@oh-my-pi/pi-coding-agent 13.5.8 → 13.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +30 -1
- package/package.json +7 -7
- package/src/cli/args.ts +7 -0
- package/src/cli/stats-cli.ts +5 -0
- package/src/config/model-registry.ts +99 -9
- package/src/config/settings-schema.ts +22 -2
- package/src/extensibility/extensions/types.ts +2 -0
- package/src/internal-urls/docs-index.generated.ts +2 -2
- package/src/internal-urls/index.ts +2 -1
- package/src/internal-urls/mcp-protocol.ts +156 -0
- package/src/internal-urls/router.ts +1 -1
- package/src/internal-urls/types.ts +3 -3
- package/src/mcp/client.ts +235 -2
- package/src/mcp/index.ts +1 -1
- package/src/mcp/manager.ts +399 -5
- package/src/mcp/oauth-flow.ts +26 -1
- package/src/mcp/smithery-auth.ts +104 -0
- package/src/mcp/smithery-connect.ts +145 -0
- package/src/mcp/smithery-registry.ts +455 -0
- package/src/mcp/types.ts +140 -0
- package/src/modes/components/footer.ts +10 -4
- package/src/modes/components/settings-defs.ts +15 -1
- package/src/modes/components/status-line/git-utils.ts +42 -0
- package/src/modes/components/status-line/presets.ts +6 -6
- package/src/modes/components/status-line/segments.ts +27 -4
- package/src/modes/components/status-line/types.ts +2 -0
- package/src/modes/components/status-line-segment-editor.ts +1 -0
- package/src/modes/components/status-line.ts +109 -5
- package/src/modes/controllers/command-controller.ts +12 -2
- package/src/modes/controllers/extension-ui-controller.ts +12 -21
- package/src/modes/controllers/mcp-command-controller.ts +577 -14
- package/src/modes/controllers/selector-controller.ts +5 -0
- package/src/modes/theme/theme.ts +6 -0
- package/src/prompts/tools/hashline.md +4 -3
- package/src/sdk.ts +115 -3
- package/src/session/agent-session.ts +19 -4
- package/src/session/session-manager.ts +17 -5
- package/src/slash-commands/builtin-registry.ts +10 -0
- package/src/task/executor.ts +37 -3
- package/src/task/index.ts +37 -5
- package/src/task/isolation-backend.ts +72 -0
- package/src/task/render.ts +6 -1
- package/src/task/types.ts +1 -0
- package/src/task/worktree.ts +67 -5
- package/src/tools/index.ts +1 -1
- package/src/tools/path-utils.ts +2 -1
- package/src/tools/read.ts +3 -7
- package/src/utils/open.ts +1 -1
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
const SMITHERY_API_BASE_URL = (process.env.SMITHERY_API_URL || "https://api.smithery.ai").replace(/\/+$/, "");
|
|
2
|
+
|
|
3
|
+
export class SmitheryConnectError extends Error {
|
|
4
|
+
status: number;
|
|
5
|
+
|
|
6
|
+
constructor(message: string, status: number) {
|
|
7
|
+
super(message);
|
|
8
|
+
this.name = "SmitheryConnectError";
|
|
9
|
+
this.status = status;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
type SmitheryNamespace = {
|
|
14
|
+
name: string;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
type SmitheryNamespacesResponse = {
|
|
18
|
+
namespaces?: SmitheryNamespace[];
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
type SmitheryConnectionStatus =
|
|
22
|
+
| { state: "connected" }
|
|
23
|
+
| { state: "auth_required"; authorizationUrl?: string }
|
|
24
|
+
| { state: "error"; message: string }
|
|
25
|
+
| { state: string; [key: string]: unknown };
|
|
26
|
+
|
|
27
|
+
export type SmitheryConnection = {
|
|
28
|
+
connectionId: string;
|
|
29
|
+
mcpUrl: string;
|
|
30
|
+
name: string;
|
|
31
|
+
status?: SmitheryConnectionStatus;
|
|
32
|
+
createdAt?: string;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
type SmitheryConnectionsResponse = {
|
|
36
|
+
connections?: SmitheryConnection[];
|
|
37
|
+
nextCursor?: string | null;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
function buildAuthHeaders(apiKey: string): Headers {
|
|
41
|
+
const headers = new Headers();
|
|
42
|
+
headers.set("Authorization", `Bearer ${apiKey}`);
|
|
43
|
+
headers.set("Content-Type", "application/json");
|
|
44
|
+
return headers;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function toApiUrl(path: string): string {
|
|
48
|
+
return `${SMITHERY_API_BASE_URL}${path}`;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function expectOk(response: Response, context: string): Promise<void> {
|
|
52
|
+
if (response.ok) return;
|
|
53
|
+
const responseText = await response.text().catch(() => "");
|
|
54
|
+
const suffix = responseText ? `: ${responseText}` : "";
|
|
55
|
+
throw new SmitheryConnectError(`${context}: ${response.status} ${response.statusText}${suffix}`, response.status);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function getSmitheryApiBaseUrl(): string {
|
|
59
|
+
return SMITHERY_API_BASE_URL;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export async function listSmitheryNamespaces(apiKey: string): Promise<SmitheryNamespace[]> {
|
|
63
|
+
const response = await fetch(toApiUrl("/namespaces"), {
|
|
64
|
+
headers: buildAuthHeaders(apiKey),
|
|
65
|
+
});
|
|
66
|
+
await expectOk(response, "Failed to list Smithery namespaces");
|
|
67
|
+
const payload = (await response.json()) as SmitheryNamespacesResponse;
|
|
68
|
+
return payload.namespaces ?? [];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export async function createSmitheryNamespace(apiKey: string): Promise<SmitheryNamespace> {
|
|
72
|
+
const response = await fetch(toApiUrl("/namespaces"), {
|
|
73
|
+
method: "POST",
|
|
74
|
+
headers: buildAuthHeaders(apiKey),
|
|
75
|
+
});
|
|
76
|
+
await expectOk(response, "Failed to create Smithery namespace");
|
|
77
|
+
return (await response.json()) as SmitheryNamespace;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export async function resolveSmitheryNamespace(apiKey: string): Promise<string> {
|
|
81
|
+
const namespaces = await listSmitheryNamespaces(apiKey);
|
|
82
|
+
if (namespaces.length > 0) {
|
|
83
|
+
return namespaces[0]?.name ?? "";
|
|
84
|
+
}
|
|
85
|
+
const created = await createSmitheryNamespace(apiKey);
|
|
86
|
+
return created.name;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export async function listSmitheryConnectionsByUrl(
|
|
90
|
+
apiKey: string,
|
|
91
|
+
namespace: string,
|
|
92
|
+
mcpUrl: string,
|
|
93
|
+
): Promise<SmitheryConnection[]> {
|
|
94
|
+
const endpoint = new URL(toApiUrl(`/connect/${encodeURIComponent(namespace)}`));
|
|
95
|
+
endpoint.searchParams.set("mcpUrl", mcpUrl);
|
|
96
|
+
const response = await fetch(endpoint.toString(), {
|
|
97
|
+
headers: buildAuthHeaders(apiKey),
|
|
98
|
+
});
|
|
99
|
+
await expectOk(response, "Failed to list Smithery connections");
|
|
100
|
+
const payload = (await response.json()) as SmitheryConnectionsResponse;
|
|
101
|
+
return payload.connections ?? [];
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export async function createSmitheryConnection(
|
|
105
|
+
apiKey: string,
|
|
106
|
+
namespace: string,
|
|
107
|
+
params: { mcpUrl: string; name?: string },
|
|
108
|
+
): Promise<SmitheryConnection> {
|
|
109
|
+
const response = await fetch(toApiUrl(`/connect/${encodeURIComponent(namespace)}`), {
|
|
110
|
+
method: "POST",
|
|
111
|
+
headers: buildAuthHeaders(apiKey),
|
|
112
|
+
body: JSON.stringify({
|
|
113
|
+
mcpUrl: params.mcpUrl,
|
|
114
|
+
name: params.name,
|
|
115
|
+
}),
|
|
116
|
+
});
|
|
117
|
+
await expectOk(response, "Failed to create Smithery connection");
|
|
118
|
+
return (await response.json()) as SmitheryConnection;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export async function getSmitheryConnection(
|
|
122
|
+
apiKey: string,
|
|
123
|
+
namespace: string,
|
|
124
|
+
connectionId: string,
|
|
125
|
+
): Promise<SmitheryConnection> {
|
|
126
|
+
const response = await fetch(
|
|
127
|
+
toApiUrl(`/connect/${encodeURIComponent(namespace)}/${encodeURIComponent(connectionId)}`),
|
|
128
|
+
{
|
|
129
|
+
headers: buildAuthHeaders(apiKey),
|
|
130
|
+
},
|
|
131
|
+
);
|
|
132
|
+
await expectOk(response, "Failed to get Smithery connection");
|
|
133
|
+
return (await response.json()) as SmitheryConnection;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export async function deleteSmitheryConnection(apiKey: string, namespace: string, connectionId: string): Promise<void> {
|
|
137
|
+
const response = await fetch(
|
|
138
|
+
toApiUrl(`/connect/${encodeURIComponent(namespace)}/${encodeURIComponent(connectionId)}`),
|
|
139
|
+
{
|
|
140
|
+
method: "DELETE",
|
|
141
|
+
headers: buildAuthHeaders(apiKey),
|
|
142
|
+
},
|
|
143
|
+
);
|
|
144
|
+
await expectOk(response, "Failed to delete Smithery connection");
|
|
145
|
+
}
|
|
@@ -0,0 +1,455 @@
|
|
|
1
|
+
import type { MCPServerConfig } from "./types";
|
|
2
|
+
|
|
3
|
+
const SMITHERY_REGISTRY_BASE_URL = "https://registry.smithery.ai";
|
|
4
|
+
|
|
5
|
+
type SmitherySearchEntry = {
|
|
6
|
+
id?: string;
|
|
7
|
+
qualifiedName?: string;
|
|
8
|
+
namespace?: string;
|
|
9
|
+
slug?: string;
|
|
10
|
+
displayName?: string;
|
|
11
|
+
description?: string;
|
|
12
|
+
remote?: boolean;
|
|
13
|
+
score?: number;
|
|
14
|
+
useCount?: number;
|
|
15
|
+
homepage?: string;
|
|
16
|
+
verified?: boolean;
|
|
17
|
+
isDeployed?: boolean;
|
|
18
|
+
createdAt?: string;
|
|
19
|
+
owner?: string;
|
|
20
|
+
iconUrl?: string;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
type SmitheryConnection = {
|
|
24
|
+
type?: "http" | "stdio";
|
|
25
|
+
deploymentUrl?: string;
|
|
26
|
+
configSchema?: SmitheryConfigSchema;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
type SmitheryConfigSchema = {
|
|
30
|
+
type?: string;
|
|
31
|
+
required?: string[];
|
|
32
|
+
properties?: Record<string, SmitheryConfigProperty>;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
type SmitheryConfigProperty = {
|
|
36
|
+
type?: string;
|
|
37
|
+
description?: string;
|
|
38
|
+
default?: unknown;
|
|
39
|
+
enum?: unknown[];
|
|
40
|
+
format?: string;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
type SmitheryServerDetails = {
|
|
44
|
+
qualifiedName?: string;
|
|
45
|
+
displayName?: string;
|
|
46
|
+
description?: string;
|
|
47
|
+
remote?: boolean;
|
|
48
|
+
deploymentUrl?: string;
|
|
49
|
+
connections?: SmitheryConnection[];
|
|
50
|
+
security?: unknown;
|
|
51
|
+
tools?: unknown;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
type SmitheryToolDefinition = {
|
|
55
|
+
name?: string;
|
|
56
|
+
description?: string;
|
|
57
|
+
inputSchema?: {
|
|
58
|
+
type?: string;
|
|
59
|
+
properties?: Record<string, unknown>;
|
|
60
|
+
required?: string[];
|
|
61
|
+
};
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
type RegistryInputType = "string" | "number" | "boolean";
|
|
65
|
+
|
|
66
|
+
export type SmitherySearchResult = {
|
|
67
|
+
id: string;
|
|
68
|
+
name: string;
|
|
69
|
+
title?: string;
|
|
70
|
+
description?: string;
|
|
71
|
+
score?: number;
|
|
72
|
+
useCount?: number;
|
|
73
|
+
display: {
|
|
74
|
+
displayName: string;
|
|
75
|
+
description: string;
|
|
76
|
+
useCount: number;
|
|
77
|
+
verified: boolean;
|
|
78
|
+
deployed: boolean;
|
|
79
|
+
transport: string;
|
|
80
|
+
connectionType: string;
|
|
81
|
+
createdAt?: string;
|
|
82
|
+
homepage?: string;
|
|
83
|
+
tools: Array<{
|
|
84
|
+
name: string;
|
|
85
|
+
description?: string;
|
|
86
|
+
params: string[];
|
|
87
|
+
}>;
|
|
88
|
+
};
|
|
89
|
+
sourceType: "remote" | "package";
|
|
90
|
+
config: MCPServerConfig;
|
|
91
|
+
warnings: string[];
|
|
92
|
+
requiredInputs: Array<{
|
|
93
|
+
key: string;
|
|
94
|
+
label: string;
|
|
95
|
+
type: RegistryInputType;
|
|
96
|
+
required: boolean;
|
|
97
|
+
defaultValue?: string;
|
|
98
|
+
description?: string;
|
|
99
|
+
enumValues?: string[];
|
|
100
|
+
sensitive: boolean;
|
|
101
|
+
}>;
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
export interface SmitherySearchOptions {
|
|
105
|
+
limit?: number;
|
|
106
|
+
apiKey?: string;
|
|
107
|
+
includeSemantic?: boolean;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export class SmitheryRegistryError extends Error {
|
|
111
|
+
status: number;
|
|
112
|
+
|
|
113
|
+
constructor(message: string, status: number) {
|
|
114
|
+
super(message);
|
|
115
|
+
this.name = "SmitheryRegistryError";
|
|
116
|
+
this.status = status;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function clampLimit(limit: number | undefined): number {
|
|
121
|
+
if (!limit || Number.isNaN(limit)) return 20;
|
|
122
|
+
if (limit < 1) return 1;
|
|
123
|
+
if (limit > 100) return 100;
|
|
124
|
+
return Math.trunc(limit);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function matchesIdentityQuery(query: string, entry: SmitherySearchEntry): boolean {
|
|
128
|
+
const normalizedQuery = query.trim().toLowerCase();
|
|
129
|
+
if (!normalizedQuery) return true;
|
|
130
|
+
const displayName = entry.displayName?.toLowerCase() ?? "";
|
|
131
|
+
const qualifiedName = entry.qualifiedName?.toLowerCase() ?? "";
|
|
132
|
+
return displayName.includes(normalizedQuery) || qualifiedName.includes(normalizedQuery);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function resolveDetailPathCandidates(entry: SmitherySearchEntry): string[] {
|
|
136
|
+
const candidates: string[] = [];
|
|
137
|
+
const pushUnique = (value: string | undefined): void => {
|
|
138
|
+
if (!value) return;
|
|
139
|
+
if (!candidates.includes(value)) candidates.push(value);
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
if (entry.namespace && entry.slug) {
|
|
143
|
+
pushUnique(`${entry.namespace}/${entry.slug}`);
|
|
144
|
+
}
|
|
145
|
+
if (entry.slug) {
|
|
146
|
+
pushUnique(entry.slug);
|
|
147
|
+
}
|
|
148
|
+
const qualifiedName = entry.qualifiedName?.trim();
|
|
149
|
+
if (qualifiedName) {
|
|
150
|
+
pushUnique(qualifiedName.replace(/^@/, ""));
|
|
151
|
+
}
|
|
152
|
+
return candidates;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function getEntryIdentityKey(entry: SmitherySearchEntry): string | null {
|
|
156
|
+
const candidates = resolveDetailPathCandidates(entry);
|
|
157
|
+
if (candidates.length > 0) {
|
|
158
|
+
return candidates[0] ?? null;
|
|
159
|
+
}
|
|
160
|
+
if (entry.id) return `id:${entry.id}`;
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function toConfigNameFromQualifiedName(qualifiedName: string): string {
|
|
165
|
+
const normalized = qualifiedName
|
|
166
|
+
.toLowerCase()
|
|
167
|
+
.replace(/^@/, "")
|
|
168
|
+
.replace(/\//g, "-")
|
|
169
|
+
.replace(/[^a-z0-9_.-]+/g, "-")
|
|
170
|
+
.replace(/-+/g, "-")
|
|
171
|
+
.replace(/^-+|-+$/g, "");
|
|
172
|
+
return normalized.length > 0 ? normalized : "mcp-server";
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function normalizeQualifiedName(value: string): string {
|
|
176
|
+
return value.startsWith("@") ? value : `@${value}`;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function scalarToString(value: unknown): string | undefined {
|
|
180
|
+
if (typeof value === "string") return value;
|
|
181
|
+
if (typeof value === "number" || typeof value === "boolean") return String(value);
|
|
182
|
+
return undefined;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function unknownToString(value: unknown): string | undefined {
|
|
186
|
+
if (value === null || value === undefined) return undefined;
|
|
187
|
+
if (typeof value === "string") return value;
|
|
188
|
+
if (typeof value === "number" || typeof value === "boolean") return String(value);
|
|
189
|
+
try {
|
|
190
|
+
return JSON.stringify(value);
|
|
191
|
+
} catch {
|
|
192
|
+
return undefined;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function safeMetadataValue(value: unknown): string | undefined {
|
|
197
|
+
const raw = unknownToString(value);
|
|
198
|
+
if (!raw) return undefined;
|
|
199
|
+
const normalized = raw
|
|
200
|
+
.replace(/[\r\n\t]+/g, " ")
|
|
201
|
+
.replace(/\s+/g, " ")
|
|
202
|
+
.trim();
|
|
203
|
+
return normalized.length > 0 ? normalized : undefined;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function toDateLabel(value: string | undefined): string | undefined {
|
|
207
|
+
if (!value) return undefined;
|
|
208
|
+
const date = new Date(value);
|
|
209
|
+
if (Number.isNaN(date.getTime())) return undefined;
|
|
210
|
+
return date.toISOString().slice(0, 10);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function getToolsList(tools: unknown): SmitherySearchResult["display"]["tools"] {
|
|
214
|
+
if (!Array.isArray(tools)) return [];
|
|
215
|
+
const output: SmitherySearchResult["display"]["tools"] = [];
|
|
216
|
+
for (const item of tools) {
|
|
217
|
+
const tool = item as SmitheryToolDefinition;
|
|
218
|
+
const name = safeMetadataValue(tool.name);
|
|
219
|
+
if (!name) continue;
|
|
220
|
+
const description = safeMetadataValue(tool.description);
|
|
221
|
+
const params = tool.inputSchema?.properties ? Object.keys(tool.inputSchema.properties) : [];
|
|
222
|
+
output.push({
|
|
223
|
+
name,
|
|
224
|
+
description,
|
|
225
|
+
params,
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
return output;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function getInputType(propertyType: string | undefined): RegistryInputType {
|
|
232
|
+
if (propertyType === "number" || propertyType === "integer") return "number";
|
|
233
|
+
if (propertyType === "boolean") return "boolean";
|
|
234
|
+
return "string";
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function isSensitiveInput(key: string, format: string | undefined): boolean {
|
|
238
|
+
if (format?.toLowerCase() === "password") return true;
|
|
239
|
+
return /(api[_-]?key|token|secret|password)/i.test(key);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function getSchemaInputs(schema: SmitheryConfigSchema | undefined): SmitherySearchResult["requiredInputs"] {
|
|
243
|
+
const required = new Set(schema?.required ?? []);
|
|
244
|
+
const properties = schema?.properties ?? {};
|
|
245
|
+
const inputs: SmitherySearchResult["requiredInputs"] = [];
|
|
246
|
+
|
|
247
|
+
for (const [key, property] of Object.entries(properties)) {
|
|
248
|
+
const type = getInputType(property.type);
|
|
249
|
+
const enumValues = Array.isArray(property.enum)
|
|
250
|
+
? property.enum.map(scalarToString).filter((value): value is string => Boolean(value))
|
|
251
|
+
: undefined;
|
|
252
|
+
inputs.push({
|
|
253
|
+
key,
|
|
254
|
+
label: key.replace(/[_-]+/g, " "),
|
|
255
|
+
type,
|
|
256
|
+
required: required.has(key),
|
|
257
|
+
defaultValue: scalarToString(property.default),
|
|
258
|
+
description: property.description,
|
|
259
|
+
enumValues: enumValues && enumValues.length > 0 ? enumValues : undefined,
|
|
260
|
+
sensitive: isSensitiveInput(key, property.format),
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
return inputs;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function chooseConnection(
|
|
268
|
+
details: SmitheryServerDetails,
|
|
269
|
+
): { connection: SmitheryConnection; useDirectHttp: boolean } | null {
|
|
270
|
+
const connections = details.connections ?? [];
|
|
271
|
+
const httpConnection = connections.find(connection => connection.type === "http" && !!connection.deploymentUrl);
|
|
272
|
+
if (httpConnection) {
|
|
273
|
+
const hasConfigInputs = getSchemaInputs(httpConnection.configSchema).length > 0;
|
|
274
|
+
if (!hasConfigInputs) {
|
|
275
|
+
return { connection: httpConnection, useDirectHttp: true };
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const stdioConnection = connections.find(connection => connection.type === "stdio");
|
|
280
|
+
if (stdioConnection) {
|
|
281
|
+
return { connection: stdioConnection, useDirectHttp: false };
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
if (httpConnection) {
|
|
285
|
+
return { connection: httpConnection, useDirectHttp: false };
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
return null;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function createConfig(
|
|
292
|
+
qualifiedName: string,
|
|
293
|
+
selected: { connection: SmitheryConnection; useDirectHttp: boolean },
|
|
294
|
+
): MCPServerConfig | null {
|
|
295
|
+
if (selected.useDirectHttp && selected.connection.type === "http" && selected.connection.deploymentUrl) {
|
|
296
|
+
return {
|
|
297
|
+
type: "http",
|
|
298
|
+
url: selected.connection.deploymentUrl,
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
return {
|
|
303
|
+
type: "stdio",
|
|
304
|
+
command: "bunx",
|
|
305
|
+
args: ["-y", "@smithery/cli", "run", normalizeQualifiedName(qualifiedName), "--config", "{}"],
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
async function fetchServerDetails(path: string, options?: { apiKey?: string }): Promise<SmitheryServerDetails | null> {
|
|
310
|
+
const headers = new Headers();
|
|
311
|
+
if (options?.apiKey) {
|
|
312
|
+
headers.set("Authorization", `Bearer ${options.apiKey}`);
|
|
313
|
+
}
|
|
314
|
+
const response = await fetch(`${SMITHERY_REGISTRY_BASE_URL}/servers/${path}`, {
|
|
315
|
+
headers,
|
|
316
|
+
});
|
|
317
|
+
if (!response.ok) return null;
|
|
318
|
+
return (await response.json()) as SmitheryServerDetails;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
async function fetchServerDetailsFromEntry(
|
|
322
|
+
entry: SmitherySearchEntry,
|
|
323
|
+
options?: { apiKey?: string },
|
|
324
|
+
): Promise<SmitheryServerDetails | null> {
|
|
325
|
+
const candidates = resolveDetailPathCandidates(entry);
|
|
326
|
+
for (const candidate of candidates) {
|
|
327
|
+
const details = await fetchServerDetails(candidate, options);
|
|
328
|
+
if (details) return details;
|
|
329
|
+
}
|
|
330
|
+
return null;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function toSearchResult(entry: SmitherySearchEntry, details: SmitheryServerDetails): SmitherySearchResult | null {
|
|
334
|
+
if (!entry.id) return null;
|
|
335
|
+
const qualifiedName = normalizeQualifiedName(
|
|
336
|
+
details.qualifiedName ?? entry.qualifiedName ?? `${entry.namespace}/${entry.slug}`,
|
|
337
|
+
);
|
|
338
|
+
const selected = chooseConnection(details);
|
|
339
|
+
if (!selected) return null;
|
|
340
|
+
|
|
341
|
+
const config = createConfig(qualifiedName, selected);
|
|
342
|
+
if (!config) return null;
|
|
343
|
+
|
|
344
|
+
const requiredInputs = getSchemaInputs(selected.connection.configSchema);
|
|
345
|
+
const warnings: string[] = [];
|
|
346
|
+
if (config.type === "stdio") {
|
|
347
|
+
warnings.push("Runs through Smithery CLI at runtime (`bunx @smithery/cli run ...`).");
|
|
348
|
+
}
|
|
349
|
+
if (requiredInputs.length > 0) {
|
|
350
|
+
warnings.push("Provider requires configuration input defined by Smithery schema.");
|
|
351
|
+
}
|
|
352
|
+
const displayName = safeMetadataValue(details.displayName ?? entry.displayName) ?? qualifiedName.replace(/^@/, "");
|
|
353
|
+
const description = safeMetadataValue(details.description ?? entry.description) ?? "No description";
|
|
354
|
+
const connectionType = safeMetadataValue(selected.connection.type) ?? "unknown";
|
|
355
|
+
const transport = safeMetadataValue(config.type ?? "stdio") ?? "stdio";
|
|
356
|
+
const createdAt = toDateLabel(entry.createdAt);
|
|
357
|
+
const homepage = safeMetadataValue(entry.homepage);
|
|
358
|
+
const tools = getToolsList(details.tools);
|
|
359
|
+
|
|
360
|
+
return {
|
|
361
|
+
id: entry.id,
|
|
362
|
+
name: qualifiedName.replace(/^@/, ""),
|
|
363
|
+
title: details.displayName ?? entry.displayName,
|
|
364
|
+
description: details.description ?? entry.description,
|
|
365
|
+
score: entry.score,
|
|
366
|
+
useCount: entry.useCount,
|
|
367
|
+
display: {
|
|
368
|
+
displayName,
|
|
369
|
+
description,
|
|
370
|
+
useCount: entry.useCount ?? 0,
|
|
371
|
+
verified: entry.verified === true,
|
|
372
|
+
deployed: entry.isDeployed === true,
|
|
373
|
+
transport,
|
|
374
|
+
connectionType,
|
|
375
|
+
createdAt,
|
|
376
|
+
homepage,
|
|
377
|
+
tools,
|
|
378
|
+
},
|
|
379
|
+
sourceType: selected.useDirectHttp || details.remote ? "remote" : "package",
|
|
380
|
+
config,
|
|
381
|
+
requiredInputs,
|
|
382
|
+
warnings,
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
export async function searchSmitheryRegistry(
|
|
387
|
+
keyword: string,
|
|
388
|
+
options?: SmitherySearchOptions,
|
|
389
|
+
): Promise<SmitherySearchResult[]> {
|
|
390
|
+
const query = keyword.trim();
|
|
391
|
+
if (!query) return [];
|
|
392
|
+
|
|
393
|
+
const limit = clampLimit(options?.limit);
|
|
394
|
+
const isSemantic = options?.includeSemantic === true;
|
|
395
|
+
const pageSize = Math.max(limit * 2, 20);
|
|
396
|
+
const headers = new Headers();
|
|
397
|
+
if (options?.apiKey) {
|
|
398
|
+
headers.set("Authorization", `Bearer ${options.apiKey}`);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// Fetch pages until we have enough filtered entries or run out of results.
|
|
402
|
+
const maxPages = 3;
|
|
403
|
+
const allEntries: SmitherySearchEntry[] = [];
|
|
404
|
+
for (let page = 1; page <= maxPages; page++) {
|
|
405
|
+
const url = new URL(`${SMITHERY_REGISTRY_BASE_URL}/servers`);
|
|
406
|
+
url.searchParams.set("q", query);
|
|
407
|
+
url.searchParams.set("pageSize", String(pageSize));
|
|
408
|
+
if (page > 1) url.searchParams.set("page", String(page));
|
|
409
|
+
const response = await fetch(url.toString(), { headers });
|
|
410
|
+
if (!response.ok) {
|
|
411
|
+
throw new SmitheryRegistryError(`Smithery search failed with status ${response.status}`, response.status);
|
|
412
|
+
}
|
|
413
|
+
const payload = (await response.json()) as { servers?: SmitherySearchEntry[] };
|
|
414
|
+
const pageEntries = payload.servers ?? [];
|
|
415
|
+
if (pageEntries.length === 0) break;
|
|
416
|
+
allEntries.push(...pageEntries);
|
|
417
|
+
|
|
418
|
+
// Stop early if we already have enough identity-matching entries.
|
|
419
|
+
const filtered = isSemantic ? allEntries : allEntries.filter(entry => matchesIdentityQuery(query, entry));
|
|
420
|
+
if (filtered.length >= limit * 2) break;
|
|
421
|
+
if (pageEntries.length < pageSize) break;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
const entries = isSemantic ? [...allEntries] : [...allEntries].filter(entry => matchesIdentityQuery(query, entry));
|
|
425
|
+
|
|
426
|
+
// Only apply local useCount sort when not in semantic mode (preserve API relevance ranking).
|
|
427
|
+
if (!isSemantic) {
|
|
428
|
+
entries.sort((a, b) => (b.useCount ?? 0) - (a.useCount ?? 0));
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
const uniqueEntries = entries.filter((entry, index) => {
|
|
432
|
+
const identity = getEntryIdentityKey(entry);
|
|
433
|
+
if (!identity) return false;
|
|
434
|
+
return (
|
|
435
|
+
entries.findIndex(candidate => {
|
|
436
|
+
const candidateIdentity = getEntryIdentityKey(candidate);
|
|
437
|
+
return candidateIdentity === identity;
|
|
438
|
+
}) === index
|
|
439
|
+
);
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
const results = await Promise.all(
|
|
443
|
+
uniqueEntries.map(async entry => {
|
|
444
|
+
const details = await fetchServerDetailsFromEntry(entry, { apiKey: options?.apiKey });
|
|
445
|
+
if (!details) return null;
|
|
446
|
+
return toSearchResult(entry, details);
|
|
447
|
+
}),
|
|
448
|
+
);
|
|
449
|
+
|
|
450
|
+
return results.filter((result): result is SmitherySearchResult => result !== null).slice(0, limit);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
export function toConfigName(candidate: string): string {
|
|
454
|
+
return toConfigNameFromQualifiedName(candidate);
|
|
455
|
+
}
|