-
Notifications
You must be signed in to change notification settings - Fork 430
[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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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"; | ||
|
||
|
@@ -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, { | ||
|
@@ -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(", ")}) `); | ||
|
||
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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. note: Claude 4 generated There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
? { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
/** | ||
* 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. | ||
|
@@ -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>; |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -16,4 +16,5 @@ export const ANSI = { | |
GREEN: "\x1b[32m", | ||
RED: "\x1b[31m", | ||
RESET: "\x1b[0m", | ||
YELLOW: "\x1b[33m", | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am having rendering issues without this (it doesn't display the variable)
There was a problem hiding this comment.
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:
Might be related to having two STDIO servers (so probably not related specifically to this PR).