Skip to content

[tiny-agents] Handle env variables in tiny-agents (JS client) #1501

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Jun 3, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
112 changes: 110 additions & 2 deletions packages/tiny-agents/src/cli.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
#!/usr/bin/env node
import { parseArgs } from "node:util";
import * as readline from "node:readline/promises";
import { stdin, stdout } from "node:process";
import { z } from "zod";
import { PROVIDERS_OR_POLICIES } from "@huggingface/inference";
import { Agent } from "@huggingface/mcp-client";
import { version as packageVersion } from "../package.json";
import { ServerConfigSchema } from "./lib/types";
import { debug, error } from "./lib/utils";
import { InputConfigSchema, ServerConfigSchema } from "./lib/types";
import { debug, error, ANSI } from "./lib/utils";
import { mainCliLoop } from "./lib/mainCliLoop";
import { loadConfigFrom } from "./lib/loadConfigFrom";

Expand Down Expand Up @@ -70,6 +72,7 @@ async function main() {
provider: z.enum(PROVIDERS_OR_POLICIES).optional(),
endpointUrl: z.string().optional(),
apiKey: z.string().optional(),
inputs: z.array(InputConfigSchema).optional(),
servers: z.array(ServerConfigSchema),
})
.refine((data) => data.provider !== undefined || data.endpointUrl !== undefined, {
Expand All @@ -85,6 +88,111 @@ async function main() {
process.exit(1);
}

// Handle inputs (i.e. env variables injection)
if (config.inputs && config.inputs.length > 0) {
const rl = readline.createInterface({ input: stdin, output: stdout });

stdout.write(ANSI.BLUE);
stdout.write("Some initial inputs are required by the agent. ");
stdout.write("Please provide a value or leave empty to load from env.");
stdout.write(ANSI.RESET);
stdout.write("\n");

for (const inputItem of config.inputs) {
const inputId = inputItem.id;
const description = inputItem.description;
const envSpecialValue = `\${input:${inputId}}`; // Special value to indicate env variable injection

// Check env variables that will use this input
const inputVars = new Set<string>();
for (const server of config.servers) {
if (server.type === "stdio" && server.config.env) {
for (const [key, value] of Object.entries(server.config.env)) {
if (value === envSpecialValue) {
inputVars.add(key);
}
}
}
if ((server.type === "http" || server.type === "sse") && server.config.options?.requestInit?.headers) {
for (const [key, value] of Object.entries(server.config.options.requestInit.headers)) {
if (value.includes(envSpecialValue)) {
inputVars.add(key);
}
}
}
}

if (inputVars.size === 0) {
stdout.write(ANSI.YELLOW);
stdout.write(`Input ${inputId} defined in config but not used by any server.`);
stdout.write(ANSI.RESET);
stdout.write("\n");
continue;
}

// Prompt user for input
stdout.write(ANSI.BLUE);
stdout.write(` • ${inputId}`);
stdout.write(ANSI.RESET);
stdout.write(`: ${description}. (default: load from ${Array.from(inputVars).join(", ")}) `);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
stdout.write(`: ${description}. (default: load from ${Array.from(inputVars).join(", ")}) `);
stdout.write("\n");

I am having rendering issues without this (it doesn't display the variable)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've reproduced the issue with this config file:

{
	"model": "Qwen/Qwen2.5-72B-Instruct",
	"provider": "nebius",
	"inputs": [
		{
			"type": "passwordInput",
			"id": "hftoken",
			"description": "Hugging Face Token",
			"password": true
		}
	],
	"servers": [
		{
			"type": "stdio",
			"config": {
				"command": "docker",
				"args": ["run", "-i", "--rm", "-e", "TRANSPORT=stdio", "-e", "HF_TOKEN=${input:hftoken}", "hf-mcp-server"],
				"env": {
					"HF_TOKEN": "${input:hftoken}"
				}
			}
		},
		{
			"type": "stdio",
			"config": {
				"command": "npx",
				"args": ["@playwright/mcp@latest"]
			}
		}
	]
}

Might be related to having two STDIO servers (so probably not related specifically to this PR).


const userInput = (await rl.question("")).trim();

// Inject user input (or env variable) into servers' env
for (const server of config.servers) {
if (server.type === "stdio" && server.config.env) {
for (const [key, value] of Object.entries(server.config.env)) {
if (value === envSpecialValue) {
if (userInput) {
server.config.env[key] = userInput;
} else {
const valueFromEnv = process.env[key] || "";
server.config.env[key] = valueFromEnv;
if (valueFromEnv) {
stdout.write(ANSI.GREEN);
stdout.write(`Value successfully loaded from '${key}'`);
stdout.write(ANSI.RESET);
stdout.write("\n");
} else {
stdout.write(ANSI.YELLOW);
stdout.write(`No value found for '${key}' in environment variables. Continuing.`);
stdout.write(ANSI.RESET);
stdout.write("\n");
}
}
}
}
}
if ((server.type === "http" || server.type === "sse") && server.config.options?.requestInit?.headers) {
for (const [key, value] of Object.entries(server.config.options.requestInit.headers)) {
if (value.includes(envSpecialValue)) {
if (userInput) {
server.config.options.requestInit.headers[key] = value.replace(envSpecialValue, userInput);
} else {
const valueFromEnv = process.env[key] || "";
server.config.options.requestInit.headers[key] = value.replace(envSpecialValue, valueFromEnv);
if (valueFromEnv) {
stdout.write(ANSI.GREEN);
stdout.write(`Value successfully loaded from '${key}'`);
stdout.write(ANSI.RESET);
stdout.write("\n");
} else {
stdout.write(ANSI.YELLOW);
stdout.write(`No value found for '${key}' in environment variables. Continuing.`);
stdout.write(ANSI.RESET);
stdout.write("\n");
}
}
}
}
}
Comment on lines +166 to +188
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

note: Claude 4 generated

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👀 well Claude could have done some refactoring efforts instead of duplicating the entire "load from env" logic ^^

}
}

stdout.write("\n");
rl.close();
}

const agent = new Agent(
config.endpointUrl
? {
Expand Down
30 changes: 29 additions & 1 deletion packages/tiny-agents/src/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,14 @@ export const ServerConfigSchema = z.discriminatedUnion("type", [
url: z.union([z.string(), z.string().url()]),
options: z
.object({
/**
* Customizes HTTP requests to the server.
*/
requestInit: z
.object({
headers: z.record(z.string()).optional(),
})
.optional(),
Comment on lines +24 to +31
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

semi-related comment for @Wauplin, @hanouticelina and @evalstate:

i am starting to think we should have gone with a simplified configuration schema (the one from VS Code, probably), rather than be more explicit and use the types from the MCP SDK, given here for instance, we need to nest the headers quite a bit inside of the config.

Maybe for a v2 we'll change this! no rush, though

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

well making the switch to something more standard right now is probably better (not in this PR but as follow-up). Agree the config nesting is not really needed

/**
* Session ID for the connection. This is used to identify the session on the server.
* When not provided and connecting to a server that supports session IDs, the server will generate a new session ID.
Expand All @@ -34,9 +42,29 @@ export const ServerConfigSchema = z.discriminatedUnion("type", [
type: z.literal("sse"),
config: z.object({
url: z.union([z.string(), z.string().url()]),
options: z.object({}).optional(),
options: z
.object({
/**
* Customizes HTTP requests to the server.
*/
requestInit: z
.object({
headers: z.record(z.string()).optional(),
})
.optional(),
})
.optional(),
}),
}),
]);

export type ServerConfig = z.infer<typeof ServerConfigSchema>;

export const InputConfigSchema = z.object({
id: z.string(),
description: z.string(),
type: z.string().optional(),
password: z.boolean().optional(),
});

export type InputConfig = z.infer<typeof InputConfigSchema>;
1 change: 1 addition & 0 deletions packages/tiny-agents/src/lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,5 @@ export const ANSI = {
GREEN: "\x1b[32m",
RED: "\x1b[31m",
RESET: "\x1b[0m",
YELLOW: "\x1b[33m",
};