Skip to content

Commit 0416cec

Browse files
Wauplinjulien-c
andauthored
[tiny-agents] Handle env variables in tiny-agents (JS client) (#1501)
This PR adds support for the inputs configuration defined by VSCode (see [docs](https://code.visualstudio.com/docs/copilot/chat/mcp-servers#_add-an-mcp-server-to-your-workspace)). PR adapted from huggingface/huggingface_hub#3129 using Cursor. Made some minor adjustments manually. ### How to test ``` pnpm run cli run wauplin/library-pr-reviewer ``` ![image](https://github.com/user-attachments/assets/f58be215-2575-4593-9ff2-5f85a6950649) --------- Co-authored-by: Julien Chaumond <[email protected]>
1 parent 5395f97 commit 0416cec

File tree

3 files changed

+140
-3
lines changed

3 files changed

+140
-3
lines changed

packages/tiny-agents/src/cli.ts

Lines changed: 110 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
#!/usr/bin/env node
22
import { parseArgs } from "node:util";
3+
import * as readline from "node:readline/promises";
4+
import { stdin, stdout } from "node:process";
35
import { z } from "zod";
46
import { PROVIDERS_OR_POLICIES } from "@huggingface/inference";
57
import { Agent } from "@huggingface/mcp-client";
68
import { version as packageVersion } from "../package.json";
7-
import { ServerConfigSchema } from "./lib/types";
8-
import { debug, error } from "./lib/utils";
9+
import { InputConfigSchema, ServerConfigSchema } from "./lib/types";
10+
import { debug, error, ANSI } from "./lib/utils";
911
import { mainCliLoop } from "./lib/mainCliLoop";
1012
import { loadConfigFrom } from "./lib/loadConfigFrom";
1113

@@ -70,6 +72,7 @@ async function main() {
7072
provider: z.enum(PROVIDERS_OR_POLICIES).optional(),
7173
endpointUrl: z.string().optional(),
7274
apiKey: z.string().optional(),
75+
inputs: z.array(InputConfigSchema).optional(),
7376
servers: z.array(ServerConfigSchema),
7477
})
7578
.refine((data) => data.provider !== undefined || data.endpointUrl !== undefined, {
@@ -85,6 +88,111 @@ async function main() {
8588
process.exit(1);
8689
}
8790

91+
// Handle inputs (i.e. env variables injection)
92+
if (config.inputs && config.inputs.length > 0) {
93+
const rl = readline.createInterface({ input: stdin, output: stdout });
94+
95+
stdout.write(ANSI.BLUE);
96+
stdout.write("Some initial inputs are required by the agent. ");
97+
stdout.write("Please provide a value or leave empty to load from env.");
98+
stdout.write(ANSI.RESET);
99+
stdout.write("\n");
100+
101+
for (const inputItem of config.inputs) {
102+
const inputId = inputItem.id;
103+
const description = inputItem.description;
104+
const envSpecialValue = `\${input:${inputId}}`; // Special value to indicate env variable injection
105+
106+
// Check env variables that will use this input
107+
const inputVars = new Set<string>();
108+
for (const server of config.servers) {
109+
if (server.type === "stdio" && server.config.env) {
110+
for (const [key, value] of Object.entries(server.config.env)) {
111+
if (value === envSpecialValue) {
112+
inputVars.add(key);
113+
}
114+
}
115+
}
116+
if ((server.type === "http" || server.type === "sse") && server.config.options?.requestInit?.headers) {
117+
for (const [key, value] of Object.entries(server.config.options.requestInit.headers)) {
118+
if (value.includes(envSpecialValue)) {
119+
inputVars.add(key);
120+
}
121+
}
122+
}
123+
}
124+
125+
if (inputVars.size === 0) {
126+
stdout.write(ANSI.YELLOW);
127+
stdout.write(`Input ${inputId} defined in config but not used by any server.`);
128+
stdout.write(ANSI.RESET);
129+
stdout.write("\n");
130+
continue;
131+
}
132+
133+
// Prompt user for input
134+
stdout.write(ANSI.BLUE);
135+
stdout.write(` • ${inputId}`);
136+
stdout.write(ANSI.RESET);
137+
stdout.write(`: ${description}. (default: load from ${Array.from(inputVars).join(", ")}) `);
138+
139+
const userInput = (await rl.question("")).trim();
140+
141+
// Inject user input (or env variable) into servers' env
142+
for (const server of config.servers) {
143+
if (server.type === "stdio" && server.config.env) {
144+
for (const [key, value] of Object.entries(server.config.env)) {
145+
if (value === envSpecialValue) {
146+
if (userInput) {
147+
server.config.env[key] = userInput;
148+
} else {
149+
const valueFromEnv = process.env[key] || "";
150+
server.config.env[key] = valueFromEnv;
151+
if (valueFromEnv) {
152+
stdout.write(ANSI.GREEN);
153+
stdout.write(`Value successfully loaded from '${key}'`);
154+
stdout.write(ANSI.RESET);
155+
stdout.write("\n");
156+
} else {
157+
stdout.write(ANSI.YELLOW);
158+
stdout.write(`No value found for '${key}' in environment variables. Continuing.`);
159+
stdout.write(ANSI.RESET);
160+
stdout.write("\n");
161+
}
162+
}
163+
}
164+
}
165+
}
166+
if ((server.type === "http" || server.type === "sse") && server.config.options?.requestInit?.headers) {
167+
for (const [key, value] of Object.entries(server.config.options.requestInit.headers)) {
168+
if (value.includes(envSpecialValue)) {
169+
if (userInput) {
170+
server.config.options.requestInit.headers[key] = value.replace(envSpecialValue, userInput);
171+
} else {
172+
const valueFromEnv = process.env[key] || "";
173+
server.config.options.requestInit.headers[key] = value.replace(envSpecialValue, valueFromEnv);
174+
if (valueFromEnv) {
175+
stdout.write(ANSI.GREEN);
176+
stdout.write(`Value successfully loaded from '${key}'`);
177+
stdout.write(ANSI.RESET);
178+
stdout.write("\n");
179+
} else {
180+
stdout.write(ANSI.YELLOW);
181+
stdout.write(`No value found for '${key}' in environment variables. Continuing.`);
182+
stdout.write(ANSI.RESET);
183+
stdout.write("\n");
184+
}
185+
}
186+
}
187+
}
188+
}
189+
}
190+
}
191+
192+
stdout.write("\n");
193+
rl.close();
194+
}
195+
88196
const agent = new Agent(
89197
config.endpointUrl
90198
? {

packages/tiny-agents/src/lib/types.ts

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,14 @@ export const ServerConfigSchema = z.discriminatedUnion("type", [
2121
url: z.union([z.string(), z.string().url()]),
2222
options: z
2323
.object({
24+
/**
25+
* Customizes HTTP requests to the server.
26+
*/
27+
requestInit: z
28+
.object({
29+
headers: z.record(z.string()).optional(),
30+
})
31+
.optional(),
2432
/**
2533
* Session ID for the connection. This is used to identify the session on the server.
2634
* When not provided and connecting to a server that supports session IDs, the server will generate a new session ID.
@@ -34,9 +42,29 @@ export const ServerConfigSchema = z.discriminatedUnion("type", [
3442
type: z.literal("sse"),
3543
config: z.object({
3644
url: z.union([z.string(), z.string().url()]),
37-
options: z.object({}).optional(),
45+
options: z
46+
.object({
47+
/**
48+
* Customizes HTTP requests to the server.
49+
*/
50+
requestInit: z
51+
.object({
52+
headers: z.record(z.string()).optional(),
53+
})
54+
.optional(),
55+
})
56+
.optional(),
3857
}),
3958
}),
4059
]);
4160

4261
export type ServerConfig = z.infer<typeof ServerConfigSchema>;
62+
63+
export const InputConfigSchema = z.object({
64+
id: z.string(),
65+
description: z.string(),
66+
type: z.string().optional(),
67+
password: z.boolean().optional(),
68+
});
69+
70+
export type InputConfig = z.infer<typeof InputConfigSchema>;

packages/tiny-agents/src/lib/utils.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,5 @@ export const ANSI = {
1616
GREEN: "\x1b[32m",
1717
RED: "\x1b[31m",
1818
RESET: "\x1b[0m",
19+
YELLOW: "\x1b[33m",
1920
};

0 commit comments

Comments
 (0)