Skip to content

Commit 9900719

Browse files
authored
Merge pull request #16 from evalstate/feat/semantic-search
Semantic Search: find HF Spaces using semantic search
2 parents e8f1d08 + bbd31cd commit 9900719

File tree

8 files changed

+1949
-682
lines changed

8 files changed

+1949
-682
lines changed

package-lock.json

Lines changed: 1682 additions & 642 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@llmindset/mcp-hfspace",
3-
"version": "0.5.1",
3+
"version": "0.5.2",
44
"description": "MCP Server to connect to Hugging Face spaces. Simple configuration, Claude Desktop friendly.",
55
"type": "module",
66
"publishConfig": {
@@ -40,23 +40,23 @@
4040
"coverage": "vitest run --coverage"
4141
},
4242
"dependencies": {
43-
"@gradio/client": "^1.8.0",
44-
"@modelcontextprotocol/sdk": "0.6.0",
45-
"mime": "^4.0.6",
43+
"@gradio/client": "^1.14.2",
44+
"@modelcontextprotocol/sdk": "1.9.0",
45+
"mime": "^4.0.7",
4646
"minimist": "^1.2.8"
4747
},
4848
"devDependencies": {
49-
"@eslint/js": "9.19.0",
49+
"@eslint/js": "9.24.0",
5050
"@types/minimist": "^1.2.5",
51-
"@types/node": "^20.11.24",
51+
"@types/node": "^22.14.1",
5252
"@typescript-eslint/eslint-plugin": "latest",
5353
"@typescript-eslint/parser": "latest",
54-
"eslint": "9.19.0",
55-
"globals": "15.14.0",
54+
"eslint": "9.24.0",
55+
"globals": "16.0.0",
5656
"prettier": "latest",
57-
"rimraf": "^5.0.1",
58-
"typescript": "^5.3.3",
59-
"typescript-eslint": "8.21.0",
60-
"vitest": "^2.1.8"
57+
"rimraf": "^6.0.1",
58+
"typescript": "^5.8.3",
59+
"typescript-eslint": "8.30.1",
60+
"vitest": "^3.1.1"
6161
}
6262
}

src/endpoint_wrapper.ts

Lines changed: 22 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ export function parsePath(path: string): EndpointPath {
4343

4444
if (parts.length != 3) {
4545
throw new Error(
46-
`Invalid Endpoint path format [${path}]. Use or vendor/space/endpoint`,
46+
`Invalid Endpoint path format [${path}]. Use or vendor/space/endpoint`
4747
);
4848
}
4949

@@ -74,19 +74,19 @@ export class EndpointWrapper {
7474
private endpointPath: EndpointPath,
7575
private endpoint: ApiEndpoint,
7676
private client: Client,
77-
private workingDir: WorkingDirectory,
77+
private workingDir: WorkingDirectory
7878
) {
7979
this.converter = new GradioConverter(workingDir);
8080
}
8181

8282
static async createEndpoint(
8383
configuredPath: string,
84-
workingDir: WorkingDirectory,
84+
workingDir: WorkingDirectory
8585
): Promise<EndpointWrapper> {
8686
const pathParts = configuredPath.split("/");
8787
if (pathParts.length < 2 || pathParts.length > 3) {
8888
throw new Error(
89-
`Invalid space path format [${configuredPath}]. Use: vendor/space or vendor/space/endpoint`,
89+
`Invalid space path format [${configuredPath}]. Use: vendor/space or vendor/space/endpoint`
9090
);
9191
}
9292

@@ -114,7 +114,7 @@ export class EndpointWrapper {
114114
if (config.debug) {
115115
await fs.writeFile(
116116
`${pathParts[0]}_${pathParts[1]}_debug_api.json`,
117-
JSON.stringify(api, null, 2),
117+
JSON.stringify(api, null, 2)
118118
);
119119
}
120120
// Try chosen API if specified
@@ -123,20 +123,20 @@ export class EndpointWrapper {
123123
parsePath(configuredPath),
124124
api.named_endpoints[endpointTarget],
125125
gradio,
126-
workingDir,
126+
workingDir
127127
);
128128
}
129129

130130
// Try preferred APIs
131131
const preferredApi = preferredApis.find(
132-
(name) => api.named_endpoints[name],
132+
(name) => api.named_endpoints[name]
133133
);
134134
if (preferredApi) {
135135
return new EndpointWrapper(
136136
parsePath(`${configuredPath}${preferredApi}`),
137137
api.named_endpoints[preferredApi],
138138
gradio,
139-
workingDir,
139+
workingDir
140140
);
141141
}
142142

@@ -147,22 +147,22 @@ export class EndpointWrapper {
147147
parsePath(`${configuredPath}${firstNamed[0]}`),
148148
firstNamed[1],
149149
gradio,
150-
workingDir,
150+
workingDir
151151
);
152152
}
153153

154154
// Try unnamed endpoints
155155
const validUnnamed = Object.entries(api.unnamed_endpoints).find(
156156
([, endpoint]) =>
157-
endpoint.parameters.length > 0 && endpoint.returns.length > 0,
157+
endpoint.parameters.length > 0 && endpoint.returns.length > 0
158158
);
159159

160160
if (validUnnamed) {
161161
return new EndpointWrapper(
162162
parsePath(`${configuredPath}/${validUnnamed[0]}`),
163163
validUnnamed[1],
164164
gradio,
165-
workingDir,
165+
workingDir
166166
);
167167
}
168168

@@ -192,7 +192,7 @@ export class EndpointWrapper {
192192

193193
async call(
194194
request: CallToolRequest,
195-
server: Server,
195+
server: Server
196196
): Promise<CallToolResult> {
197197
const progressToken = request.params._meta?.progressToken as
198198
| string
@@ -207,7 +207,7 @@ export class EndpointWrapper {
207207
// Process each parameter, applying handle_file for file inputs
208208
for (const [key, value] of Object.entries(parameters)) {
209209
const param = endpointParams.find(
210-
(p) => p.parameter_name === key || p.label === key,
210+
(p) => p.parameter_name === key || p.label === key
211211
);
212212
if (param && isFileParameter(param) && typeof value === "string") {
213213
const file = await this.validatePath(value);
@@ -226,22 +226,22 @@ export class EndpointWrapper {
226226
async handleToolCall(
227227
parameters: Record<string, unknown>,
228228
progressToken: string | undefined,
229-
server: Server,
229+
server: Server
230230
): Promise<CallToolResult> {
231231
const events = [];
232232
try {
233233
let result = null;
234234
const submission: AsyncIterable<GradioEvent> = this.client.submit(
235235
this.endpointPath.endpoint,
236-
parameters,
236+
parameters
237237
) as AsyncIterable<GradioEvent>;
238238
const progressNotifier = createProgressNotifier(server);
239239
for await (const msg of submission) {
240240
if (config.debug) events.push(msg);
241241
if (msg.type === "data") {
242242
if (Array.isArray(msg.data)) {
243243
const hasContent = msg.data.some(
244-
(item: unknown) => typeof item !== "object",
244+
(item: unknown) => typeof item !== "object"
245245
);
246246

247247
if (hasContent) result = msg.data;
@@ -262,7 +262,7 @@ export class EndpointWrapper {
262262
return await this.convertPredictResults(
263263
this.endpoint.returns,
264264
result,
265-
this.endpointPath,
265+
this.endpointPath
266266
);
267267
} catch (error) {
268268
const errorMessage =
@@ -274,7 +274,7 @@ export class EndpointWrapper {
274274
`${this.mcpToolName}_status_${crypto
275275
.randomUUID()
276276
.substring(0, 5)}.json`,
277-
JSON.stringify(events, null, 2),
277+
JSON.stringify(events, null, 2)
278278
);
279279
}
280280
}
@@ -283,7 +283,7 @@ export class EndpointWrapper {
283283
private async convertPredictResults(
284284
returns: ApiReturn[],
285285
predictResults: unknown[],
286-
endpointPath: EndpointPath,
286+
endpointPath: EndpointPath
287287
): Promise<CallToolResult> {
288288
const content: (TextContent | ImageContent | EmbeddedResource)[] = [];
289289

@@ -292,7 +292,7 @@ export class EndpointWrapper {
292292
const converted = await this.converter.convert(
293293
output,
294294
value as GradioResourceValue,
295-
endpointPath,
295+
endpointPath
296296
);
297297
content.push(converted);
298298
}
@@ -317,13 +317,13 @@ export class EndpointWrapper {
317317
name,
318318
description: prop?.description || name,
319319
required: schema.required?.includes(name) || false,
320-
}),
320+
})
321321
),
322322
};
323323
}
324324

325325
async getPromptTemplate(
326-
args?: Record<string, string>,
326+
args?: Record<string, string>
327327
): Promise<GetPromptResult> {
328328
const schema = convertApiToSchema(this.endpoint);
329329
let promptText = `Using the ${this.mcpDescriptionName()}:\n\n`;

src/index.ts

Lines changed: 60 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
const AVAILABLE_RESOURCES = "Available Resources";
44
const AVAILABLE_FILES = "available-files";
5-
5+
const SEARCH_FOR_SPACE = "search-spaces";
66
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
77
import { VERSION } from "./version.js";
88
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
@@ -19,6 +19,7 @@ import {
1919
import { EndpointWrapper } from "./endpoint_wrapper.js";
2020
import { parseConfig } from "./config.js";
2121
import { WorkingDirectory } from "./working_directory.js";
22+
import { SemanticSearch } from "./semantic_search.js";
2223

2324
// Create MCP server
2425
const server = new Server(
@@ -34,17 +35,18 @@ const server = new Server(
3435
list: true,
3536
},
3637
},
37-
},
38+
}
3839
);
3940
// Parse configuration
4041
const config = parseConfig();
42+
const semanticSearch = new SemanticSearch();
4143

4244
// Change to configured working directory
4345
process.chdir(config.workDir);
4446

4547
const workingDir = new WorkingDirectory(
4648
config.workDir,
47-
config.claudeDesktopMode,
49+
config.claudeDesktopMode
4850
);
4951

5052
// Create a map to store endpoints by their tool names
@@ -55,7 +57,7 @@ for (const spacePath of config.spacePaths) {
5557
try {
5658
const endpoint = await EndpointWrapper.createEndpoint(
5759
spacePath,
58-
workingDir,
60+
workingDir
5961
);
6062
endpoints.set(endpoint.toolDefinition().name, endpoint);
6163
} catch (e) {
@@ -87,8 +89,26 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
8789
properties: {},
8890
},
8991
},
92+
{
93+
name: SEARCH_FOR_SPACE,
94+
description:
95+
"Use semantic search to find an endpoint on the `Hugging Face Spaces` service. The search term will usually " +
96+
"be 3-7 words describing a task or activity the Person is trying to accomplish. The results are returned in a markdown table. " +
97+
"Present all results to the Person. Await specific guidance from the Person before making further Tool calls.",
98+
inputSchema: {
99+
type: "object",
100+
properties: {
101+
query: {
102+
type: "string",
103+
// TODO description assumes user is using claude desktop which has knowledge of HF Spaces.
104+
// consider updating for not CLAUDE_DESKTOP mode. 3.7 sys prompt refers to Human as Person
105+
description: "The semantic search term to use.",
106+
},
107+
},
108+
},
109+
},
90110
...Array.from(endpoints.values()).map((endpoint) =>
91-
endpoint.toolDefinition(),
111+
endpoint.toolDefinition()
92112
),
93113
],
94114
};
@@ -110,6 +130,40 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
110130
};
111131
}
112132

133+
if (SEARCH_FOR_SPACE === request.params.name) {
134+
try {
135+
const query = request.params.arguments?.query as string;
136+
if (!query || typeof query !== "string") {
137+
throw new Error("Search query must be a non-empty string");
138+
}
139+
140+
const results = await semanticSearch.search(query);
141+
const markdownTable = semanticSearch.formatSearchResults(results);
142+
143+
return {
144+
content: [
145+
{
146+
type: "text",
147+
text: markdownTable,
148+
},
149+
],
150+
};
151+
} catch (error) {
152+
if (error instanceof Error) {
153+
return {
154+
content: [
155+
{
156+
type: "text",
157+
text: `Search error: ${error.message}`,
158+
},
159+
],
160+
isError: true,
161+
};
162+
}
163+
throw error;
164+
}
165+
}
166+
113167
const endpoint = endpoints.get(request.params.name);
114168

115169
if (!endpoint) {
@@ -142,7 +196,7 @@ server.setRequestHandler(ListPromptsRequestSchema, async () => {
142196
arguments: [],
143197
},
144198
...Array.from(endpoints.values()).map((endpoint) =>
145-
endpoint.promptDefinition(),
199+
endpoint.promptDefinition()
146200
),
147201
],
148202
};

0 commit comments

Comments
 (0)