Skip to content

Commit 0e2cf94

Browse files
julien-ccoyotte508SBrandeis
authored
InferenceClient is also a McpClient (#1351)
## Required reading https://modelcontextprotocol.io/quickstart/client TL;DR: MCP is a standard API to expose sets of Tools that can be hooked to LLMs ## Summary of how to use this You can either use McpClient, or you can run an example Agent directly: # Tiny Agent We now have a tiny Agent (a while loop, really) in this PR, built on top of the MCP Client. You can run it like this: ```bash cd packages/mcp-client pnpm run agent ``` # `McpClient` i.e. the underlying class ```ts const client = new McpClient({ provider: "together", model: "Qwen/Qwen2.5-72B-Instruct", apiKey: process.env.HF_TOKEN, }); await client.addMcpServer({ // Filesystem "official" mcp-server with access to your Desktop command: "npx", args: ["-y", "@modelcontextprotocol/server-filesystem", join(homedir(), "Desktop")], }); ``` ## Variant where we call a custom, local MCP server ```ts await client.addMcpServer( "node", ["--disable-warning=ExperimentalWarning", join(homedir(), "Desktop/hf-mcp/index.ts")], { HF_TOKEN: process.env.HF_TOKEN, } ); const response = await client.processQuery(` find an app that generates 3D models from text, and also get the best paper about transformers `); ``` #### Where to find the MCP Server used here as an example https://gist.github.com/julien-c/0500ba922e1b38f2dc30447fb81f7dc6 (Note that you can replace it with any MCP Server, from this doc for instance: https://modelcontextprotocol.io/examples) ## Python version Python version will be implemented in `huggingface_hub` in this PR: huggingface/huggingface_hub#2986 Contributions are welcome! --------- Co-authored-by: Eliott C. <[email protected]> Co-authored-by: SBrandeis <[email protected]> Co-authored-by: Simon Brandeis <[email protected]>
1 parent 8514765 commit 0e2cf94

18 files changed

+1355
-5
lines changed
+72
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
name: MCP Client - Version and Release
2+
3+
on:
4+
workflow_dispatch:
5+
inputs:
6+
newversion:
7+
type: choice
8+
description: "Semantic Version Bump Type"
9+
default: patch
10+
options:
11+
- patch
12+
- minor
13+
- major
14+
15+
concurrency:
16+
group: "push-to-main"
17+
18+
defaults:
19+
run:
20+
working-directory: packages/mcp-client
21+
22+
jobs:
23+
version_and_release:
24+
runs-on: ubuntu-latest
25+
steps:
26+
- uses: actions/checkout@v3
27+
with:
28+
token: ${{ secrets.BOT_ACCESS_TOKEN }}
29+
- run: npm install -g corepack@latest && corepack enable
30+
- uses: actions/setup-node@v3
31+
with:
32+
node-version: "20"
33+
cache: "pnpm"
34+
cache-dependency-path: |
35+
packages/mcp-client/pnpm-lock.yaml
36+
packages/doc-internal/pnpm-lock.yaml
37+
registry-url: "https://registry.npmjs.org"
38+
- run: pnpm install
39+
- run: git config --global user.name machineuser
40+
- run: git config --global user.email [email protected]
41+
- run: |
42+
PACKAGE_VERSION=$(node -p "require('./package.json').version")
43+
BUMPED_VERSION=$(node -p "require('semver').inc('$PACKAGE_VERSION', '${{ github.event.inputs.newversion }}')")
44+
# Update package.json with the new version
45+
node -e "const fs = require('fs'); const package = JSON.parse(fs.readFileSync('./package.json')); package.version = '$BUMPED_VERSION'; fs.writeFileSync('./package.json', JSON.stringify(package, null, '\t') + '\n');"
46+
pnpm --filter doc-internal run fix-cdn-versions
47+
git add ../..
48+
git commit -m "🔖 @huggingface/mcp-client $BUMPED_VERSION"
49+
git tag "mcp-client-v$BUMPED_VERSION"
50+
51+
# Add checks for dependencies if needed, similar to hub-publish.yml
52+
- name: "Check Deps are published before publishing this package"
53+
run: pnpm -w check-deps inference && pnpm -w check-deps tasks
54+
55+
- run: pnpm publish --no-git-checks .
56+
env:
57+
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
58+
- run: git pull --rebase && git push --follow-tags
59+
# hack - reuse actions/setup-node@v3 just to set a new registry
60+
- uses: actions/setup-node@v3
61+
with:
62+
node-version: "20"
63+
registry-url: "https://npm.pkg.github.com"
64+
# Disable for now, until github supports PATs for writing github packages (https://github.com/github/roadmap/issues/558)
65+
# - run: pnpm publish --no-git-checks .
66+
# env:
67+
# NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
68+
- name: "Update Doc"
69+
uses: peter-evans/repository-dispatch@v2
70+
with:
71+
event-type: doc-build
72+
token: ${{ secrets.BOT_ACCESS_TOKEN }}

docs/_toctree.yml

+5
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,11 @@
1414
title: Interact with the Hub
1515
- local: hub/modules
1616
title: API Reference
17+
- title: "@huggingface/mcp-client"
18+
isExpanded: true
19+
sections:
20+
- local: mcp-client/README
21+
title: Simple MCP Client and smol Agent built on top of Inference Client
1722
- title: "@huggingface/agent"
1823
isExpanded: true
1924
sections:

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,9 @@
1111
"check-deps": "tsx scripts/check-deps.ts"
1212
},
1313
"devDependencies": {
14+
"@types/node": "^22.14.1",
1415
"@typescript-eslint/eslint-plugin": "^7.2.0",
1516
"@typescript-eslint/parser": "^7.2.0",
16-
"@types/node": "^18.16.1",
1717
"@vitest/browser": "^0.34.6",
1818
"eslint": "^8.57.0",
1919
"eslint-config-prettier": "^9.0.0",

packages/doc-internal/package.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
"description": "Package to generate doc for other @huggingface packages",
66
"private": true,
77
"scripts": {
8-
"start": "pnpm run fix-cdn-versions && pnpm run doc-hub && pnpm run doc-inference && pnpm run doc-agents && pnpm run doc-space-header && pnpm run doc-gguf && cp ../../README.md ../../docs/index.md && pnpm run update-toc && pnpm run fix-md-links && pnpm run fix-md-headinghashlinks",
8+
"start": "pnpm run fix-cdn-versions && pnpm run doc-hub && pnpm run doc-inference && pnpm run doc-agents && pnpm run doc-space-header && pnpm run doc-gguf && pnpm run doc-mcp-client && cp ../../README.md ../../docs/index.md && pnpm run update-toc && pnpm run fix-md-links && pnpm run fix-md-headinghashlinks",
99
"lint": "eslint --quiet --fix --ext .cjs,.ts .",
1010
"lint:check": "eslint --ext .cjs,.ts .",
1111
"format": "prettier --write .",
@@ -14,6 +14,7 @@
1414
"doc-inference": "typedoc --tsconfig ../inference/tsconfig.json --githubPages false --plugin typedoc-plugin-markdown --out ../../docs/inference --hideBreadcrumbs --hideInPageTOC --sourceLinkTemplate https://github.com/huggingface/huggingface.js/blob/main/{path}#L{line} ../inference/src/index.ts",
1515
"doc-agents": "typedoc --tsconfig ../agents/tsconfig.json --githubPages false --plugin typedoc-plugin-markdown --out ../../docs/agents --hideBreadcrumbs --hideInPageTOC --sourceLinkTemplate https://github.com/huggingface/huggingface.js/blob/main/{path}#L{line} ../agents/src/index.ts",
1616
"doc-gguf": "mkdir -p ../../docs/gguf && cp ../../packages/gguf/README.md ../../docs/gguf/README.md",
17+
"doc-mcp-client": "mkdir -p ../../docs/mcp-client && cp ../../packages/mcp-client/README.md ../../docs/mcp-client/README.md",
1718
"doc-space-header": "mkdir -p ../../docs/space-header && cp ../../packages/space-header/README.md ../../docs/space-header/README.md",
1819
"update-toc": "tsx update-toc.ts",
1920
"fix-cdn-versions": "tsx fix-cdn-versions.ts",

packages/mcp-client/.eslintignore

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
dist

packages/mcp-client/.prettierignore

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
pnpm-lock.yaml
2+
# In order to avoid code samples to have tabs, they don't display well on npm
3+
README.md
4+
dist

packages/mcp-client/README.md

+72
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
# @huggingface/mcp-client
2+
3+
Client for the Model Context Protocol (MCP).
4+
5+
This package provides a client implementation for interacting with MCP servers, built on top of our InferenceClient, `@huggingface/inference`.
6+
7+
It includes an example CLI smol Agent that can leverage MCP tools.
8+
9+
## Installation
10+
11+
This package is part of the Hugging Face JS monorepo. To install dependencies for all packages, run from the root of the repository:
12+
13+
```bash
14+
pnpm install
15+
```
16+
17+
## Usage (CLI Agent)
18+
19+
The package includes a command-line interface (CLI) agent that demonstrates how to use the MCP client.
20+
21+
### Prerequisites
22+
23+
* **Hugging Face API Token:** You need a Hugging Face API token with appropriate permissions. Set it as an environment variable:
24+
```bash
25+
export HF_TOKEN="hf_..."
26+
```
27+
28+
### Running the Agent
29+
30+
Navigate to the package directory and run the agent script:
31+
32+
```bash
33+
cd packages/mcp-client
34+
pnpm agent
35+
```
36+
37+
Alternatively, run from the root of the monorepo:
38+
39+
```bash
40+
pnpm --filter @huggingface/mcp-client agent
41+
```
42+
43+
The agent will load available MCP tools (by default, connecting to a filesystem server for your Desktop and a Playwright server) and prompt you for input (`>`).
44+
45+
### Configuration (Environment Variables)
46+
47+
* `HF_TOKEN` (Required): Your Hugging Face API token.
48+
* `MODEL_ID` (Optional): The model ID to use for the agent's inference. Defaults to `Qwen/Qwen2.5-72B-Instruct`.
49+
* `PROVIDER` (Optional): The inference provider. Defaults to `together`. See `@huggingface/inference` for available providers.
50+
* `EXPERIMENTAL_HF_MCP_SERVER` (Optional): Set to `true` to enable connection to an experimental Hugging Face MCP server (requires separate setup).
51+
52+
Example with custom model:
53+
54+
```bash
55+
export HF_TOKEN="hf_..."
56+
export MODEL_ID="mistralai/Mixtral-8x7B-Instruct-v0.1"
57+
pnpm agent
58+
```
59+
60+
## Development
61+
62+
Common development tasks can be run using pnpm scripts:
63+
64+
* `pnpm build`: Build the package.
65+
* `pnpm lint`: Lint and fix code style.
66+
* `pnpm format`: Format code using Prettier.
67+
* `pnpm test`: Run tests using Vitest.
68+
* `pnpm check`: Type-check the code using TypeScript.
69+
70+
## License
71+
72+
MIT

packages/mcp-client/cli.ts

+128
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
import * as readline from "node:readline/promises";
2+
import { stdin, stdout } from "node:process";
3+
import { join } from "node:path";
4+
import { homedir } from "node:os";
5+
import { Agent } from "./src";
6+
import type { StdioServerParameters } from "@modelcontextprotocol/sdk/client/stdio.js";
7+
import { ANSI } from "./src/utils";
8+
import type { InferenceProvider } from "@huggingface/inference";
9+
10+
const MODEL_ID = process.env.MODEL_ID ?? "Qwen/Qwen2.5-72B-Instruct";
11+
const PROVIDER = (process.env.PROVIDER as InferenceProvider) ?? "nebius";
12+
13+
const SERVERS: StdioServerParameters[] = [
14+
{
15+
// Filesystem "official" mcp-server with access to your Desktop
16+
command: "npx",
17+
args: ["-y", "@modelcontextprotocol/server-filesystem", join(homedir(), "Desktop")],
18+
},
19+
{
20+
// Playwright MCP
21+
command: "npx",
22+
args: ["@playwright/mcp@latest"],
23+
},
24+
];
25+
26+
if (process.env.EXPERIMENTAL_HF_MCP_SERVER) {
27+
SERVERS.push({
28+
// Early version of a HF-MCP server
29+
// you can download it from gist.github.com/julien-c/0500ba922e1b38f2dc30447fb81f7dc6
30+
command: "node",
31+
args: ["--disable-warning=ExperimentalWarning", join(homedir(), "Desktop/hf-mcp/index.ts")],
32+
env: {
33+
HF_TOKEN: process.env.HF_TOKEN ?? "",
34+
},
35+
});
36+
}
37+
38+
async function main() {
39+
if (!process.env.HF_TOKEN) {
40+
console.error(`a valid HF_TOKEN must be present in the env`);
41+
process.exit(1);
42+
}
43+
44+
const agent = new Agent({
45+
provider: PROVIDER,
46+
model: MODEL_ID,
47+
apiKey: process.env.HF_TOKEN,
48+
servers: SERVERS,
49+
});
50+
51+
const rl = readline.createInterface({ input: stdin, output: stdout });
52+
let abortController = new AbortController();
53+
let waitingForInput = false;
54+
async function waitForInput() {
55+
waitingForInput = true;
56+
const input = await rl.question("> ");
57+
waitingForInput = false;
58+
return input;
59+
}
60+
rl.on("SIGINT", async () => {
61+
if (waitingForInput) {
62+
// close the whole process
63+
await agent.cleanup();
64+
stdout.write("\n");
65+
rl.close();
66+
} else {
67+
// otherwise, it means a request is underway
68+
abortController.abort();
69+
abortController = new AbortController();
70+
stdout.write("\n");
71+
stdout.write(ANSI.GRAY);
72+
stdout.write("Ctrl+C a second time to exit");
73+
stdout.write(ANSI.RESET);
74+
stdout.write("\n");
75+
}
76+
});
77+
process.on("uncaughtException", (err) => {
78+
stdout.write("\n");
79+
rl.close();
80+
throw err;
81+
});
82+
83+
await agent.loadTools();
84+
85+
stdout.write(ANSI.BLUE);
86+
stdout.write(`Agent loaded with ${agent.availableTools.length} tools:\n`);
87+
stdout.write(agent.availableTools.map((t) => `- ${t.function.name}`).join("\n"));
88+
stdout.write(ANSI.RESET);
89+
stdout.write("\n");
90+
91+
while (true) {
92+
const input = await waitForInput();
93+
for await (const chunk of agent.run(input, { abortSignal: abortController.signal })) {
94+
if ("choices" in chunk) {
95+
const delta = chunk.choices[0]?.delta;
96+
if (delta.content) {
97+
stdout.write(delta.content);
98+
}
99+
if (delta.tool_calls) {
100+
stdout.write(ANSI.GRAY);
101+
for (const deltaToolCall of delta.tool_calls) {
102+
if (deltaToolCall.id) {
103+
stdout.write(`<Tool ${deltaToolCall.id}>\n`);
104+
}
105+
if (deltaToolCall.function.name) {
106+
stdout.write(deltaToolCall.function.name + " ");
107+
}
108+
if (deltaToolCall.function.arguments) {
109+
stdout.write(deltaToolCall.function.arguments);
110+
}
111+
}
112+
stdout.write(ANSI.RESET);
113+
}
114+
} else {
115+
/// Tool call info
116+
stdout.write("\n\n");
117+
stdout.write(ANSI.GREEN);
118+
stdout.write(`Tool[${chunk.name}] ${chunk.tool_call_id}\n`);
119+
stdout.write(chunk.content);
120+
stdout.write(ANSI.RESET);
121+
stdout.write("\n\n");
122+
}
123+
}
124+
stdout.write("\n");
125+
}
126+
}
127+
128+
main();

packages/mcp-client/package.json

+57
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
{
2+
"name": "@huggingface/mcp-client",
3+
"packageManager": "[email protected]",
4+
"version": "0.0.1",
5+
"description": "Client for the Model Context Protocol",
6+
"repository": "https://github.com/huggingface/huggingface.js.git",
7+
"publishConfig": {
8+
"access": "public"
9+
},
10+
"main": "./dist/index.js",
11+
"module": "./dist/index.mjs",
12+
"types": "./dist/index.d.ts",
13+
"exports": {
14+
".": {
15+
"types": "./dist/index.d.ts",
16+
"require": "./dist/index.js",
17+
"import": "./dist/index.mjs"
18+
}
19+
},
20+
"engines": {
21+
"node": ">=18"
22+
},
23+
"source": "index.ts",
24+
"scripts": {
25+
"lint": "eslint --quiet --fix --ext .cjs,.ts .",
26+
"lint:check": "eslint --ext .cjs,.ts .",
27+
"format": "prettier --write .",
28+
"format:check": "prettier --check .",
29+
"prepublishOnly": "pnpm run build",
30+
"build": "tsup src/index.ts --format cjs,esm --clean && tsc --emitDeclarationOnly --declaration",
31+
"prepare": "pnpm run build",
32+
"test": "vitest run",
33+
"check": "tsc",
34+
"agent": "tsx cli.ts"
35+
},
36+
"files": [
37+
"src",
38+
"dist",
39+
"index.ts",
40+
"tsconfig.json"
41+
],
42+
"keywords": [
43+
"huggingface",
44+
"model context protocol",
45+
"mcp",
46+
"client",
47+
"hugging",
48+
"face"
49+
],
50+
"author": "Hugging Face",
51+
"license": "MIT",
52+
"dependencies": {
53+
"@huggingface/inference": "workspace:^",
54+
"@huggingface/tasks": "workspace:^",
55+
"@modelcontextprotocol/sdk": "^1.9.0"
56+
}
57+
}

0 commit comments

Comments
 (0)