agency-lang 0.0.99 → 0.0.101

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/dist/lib/backends/typescriptBuilder.d.ts +5 -1
  2. package/dist/lib/backends/typescriptBuilder.js +24 -7
  3. package/dist/lib/backends/typescriptGenerator/builtins.js +3 -0
  4. package/dist/lib/backends/typescriptGenerator.d.ts +1 -1
  5. package/dist/lib/backends/typescriptGenerator.js +2 -2
  6. package/dist/lib/cli/commands.js +19 -2
  7. package/dist/lib/cli/debug.js +8 -1
  8. package/dist/lib/config.d.ts +64 -0
  9. package/dist/lib/config.js +44 -0
  10. package/dist/lib/config.test.d.ts +1 -0
  11. package/dist/lib/config.test.js +64 -0
  12. package/dist/lib/debugger/uiState.js +4 -3
  13. package/dist/lib/importPaths.d.ts +6 -2
  14. package/dist/lib/importPaths.js +14 -3
  15. package/dist/lib/importStrategy.d.ts +3 -0
  16. package/dist/lib/importStrategy.js +40 -15
  17. package/dist/lib/runtime/index.d.ts +2 -0
  18. package/dist/lib/runtime/index.js +1 -0
  19. package/dist/lib/runtime/mcp/__tests__/testServer.d.ts +1 -0
  20. package/dist/lib/runtime/mcp/__tests__/testServer.js +14 -0
  21. package/dist/lib/runtime/mcp/mcp.integration.test.d.ts +1 -0
  22. package/dist/lib/runtime/mcp/mcp.integration.test.js +37 -0
  23. package/dist/lib/runtime/mcp/mcpConnection.d.ts +14 -0
  24. package/dist/lib/runtime/mcp/mcpConnection.js +61 -0
  25. package/dist/lib/runtime/mcp/mcpConnection.test.d.ts +1 -0
  26. package/dist/lib/runtime/mcp/mcpConnection.test.js +35 -0
  27. package/dist/lib/runtime/mcp/mcpManager.d.ts +11 -0
  28. package/dist/lib/runtime/mcp/mcpManager.js +51 -0
  29. package/dist/lib/runtime/mcp/mcpManager.test.d.ts +1 -0
  30. package/dist/lib/runtime/mcp/mcpManager.test.js +70 -0
  31. package/dist/lib/runtime/mcp/toolAdapter.d.ts +6 -0
  32. package/dist/lib/runtime/mcp/toolAdapter.js +46 -0
  33. package/dist/lib/runtime/mcp/toolAdapter.test.d.ts +1 -0
  34. package/dist/lib/runtime/mcp/toolAdapter.test.js +68 -0
  35. package/dist/lib/runtime/mcp/types.d.ts +18 -0
  36. package/dist/lib/runtime/mcp/types.js +1 -0
  37. package/dist/lib/runtime/node.js +1 -0
  38. package/dist/lib/runtime/prompt.js +9 -1
  39. package/dist/lib/runtime/state/context.d.ts +5 -0
  40. package/dist/lib/runtime/state/context.js +13 -0
  41. package/dist/lib/templates/backends/typescriptGenerator/builtinFunctions/mcp.d.ts +4 -0
  42. package/dist/lib/templates/backends/typescriptGenerator/builtinFunctions/mcp.js +11 -0
  43. package/dist/lib/templates/backends/typescriptGenerator/imports.d.ts +1 -1
  44. package/dist/lib/templates/backends/typescriptGenerator/imports.js +1 -1
  45. package/dist/lib/version.d.ts +1 -1
  46. package/dist/lib/version.js +1 -1
  47. package/package.json +2 -1
  48. package/stdlib/_builtins.js +134 -0
  49. package/stdlib/_math.js +9 -0
  50. package/stdlib/_utils.js +51 -0
  51. package/stdlib/agent.js +4 -1
  52. package/stdlib/array.js +1732 -1812
  53. package/stdlib/clipboard.js +4 -1
  54. package/stdlib/fs.js +4 -1
  55. package/stdlib/http.js +4 -1
  56. package/stdlib/index.js +1860 -1875
  57. package/stdlib/math.agency +3 -5
  58. package/stdlib/math.js +8 -9
  59. package/stdlib/object.js +1160 -836
  60. package/stdlib/path.js +4 -1
  61. package/stdlib/shell.js +4 -1
  62. package/stdlib/speech.js +4 -1
  63. package/stdlib/strategy.js +4 -1
  64. package/stdlib/system.js +4 -1
  65. package/stdlib/ui.js +4 -1
  66. package/stdlib/weather.js +4 -1
  67. package/stdlib/wikipedia.js +4 -1
@@ -29,14 +29,18 @@ export declare class TypeScriptBuilder {
29
29
  private _sourceMapBuilder;
30
30
  private programInfo;
31
31
  private moduleId;
32
+ private outputFile;
32
33
  /**
33
34
  * @param config - Agency compiler configuration (model defaults, logging, etc.)
34
35
  * @param info - Pre-collected program metadata (function definitions, graph nodes, imports, type hints)
35
36
  * @param moduleId - Unique identifier for this module (e.g., "foo.agency"), used to
36
37
  * namespace global variables in the GlobalStore so that different modules' globals
37
38
  * don't collide. Must be consistent between the defining module and any importers.
39
+ * @param outputFile - Absolute path where the generated code will be written.
40
+ * Used to compute relative import paths for stdlib. If not provided, falls
41
+ * back to resolving moduleId against cwd.
38
42
  */
39
- constructor(config: AgencyConfig | undefined, info: ProgramInfo, moduleId: string);
43
+ constructor(config: AgencyConfig | undefined, info: ProgramInfo, moduleId: string, outputFile?: string);
40
44
  private configDefaults;
41
45
  /** Convert a TsNode to string (for use in template-based methods) */
42
46
  private str;
@@ -1,3 +1,4 @@
1
+ import path from "path";
1
2
  import { formatTypeHint } from "../cli/util.js";
2
3
  import { BUILTIN_FUNCTIONS, BUILTIN_TOOLS, BUILTIN_VARIABLES, TYPES_THAT_DONT_TRIGGER_NEW_PART, } from "../config.js";
3
4
  import { expressionToString } from "../utils/node.js";
@@ -75,17 +76,22 @@ export class TypeScriptBuilder {
75
76
  _sourceMapBuilder = new SourceMapBuilder();
76
77
  programInfo;
77
78
  moduleId;
79
+ outputFile;
78
80
  /**
79
81
  * @param config - Agency compiler configuration (model defaults, logging, etc.)
80
82
  * @param info - Pre-collected program metadata (function definitions, graph nodes, imports, type hints)
81
83
  * @param moduleId - Unique identifier for this module (e.g., "foo.agency"), used to
82
84
  * namespace global variables in the GlobalStore so that different modules' globals
83
85
  * don't collide. Must be consistent between the defining module and any importers.
86
+ * @param outputFile - Absolute path where the generated code will be written.
87
+ * Used to compute relative import paths for stdlib. If not provided, falls
88
+ * back to resolving moduleId against cwd.
84
89
  */
85
- constructor(config, info, moduleId) {
90
+ constructor(config, info, moduleId, outputFile) {
86
91
  this.agencyConfig = mergeDeep(this.configDefaults(), config || {});
87
92
  this.programInfo = info;
88
93
  this.moduleId = moduleId;
94
+ this.outputFile = outputFile;
89
95
  }
90
96
  configDefaults() {
91
97
  return {
@@ -1041,7 +1047,7 @@ export class TypeScriptBuilder {
1041
1047
  return ts.runnerIfElse({ id, branches, elseBranch });
1042
1048
  }
1043
1049
  processImportStatement(node) {
1044
- const from = toCompiledImportPath(node.modulePath);
1050
+ const from = toCompiledImportPath(node.modulePath, this.outputFile ?? path.resolve(this.moduleId));
1045
1051
  const imports = node.importedNames.map((nameType) => {
1046
1052
  switch (nameType.type) {
1047
1053
  case "namedImport":
@@ -1092,7 +1098,7 @@ export class TypeScriptBuilder {
1092
1098
  return ts.importDecl({
1093
1099
  importKind: "named",
1094
1100
  names: importNames,
1095
- from: toCompiledImportPath(node.agencyFile),
1101
+ from: toCompiledImportPath(node.agencyFile, this.outputFile ?? path.resolve(this.moduleId)),
1096
1102
  });
1097
1103
  }
1098
1104
  // ------- TsRaw wrapper methods (template-heavy) -------
@@ -1862,6 +1868,7 @@ export class TypeScriptBuilder {
1862
1868
  const configArg = node.arguments[1];
1863
1869
  let clientConfig;
1864
1870
  let configToolNames = [];
1871
+ let configToolExprs = [];
1865
1872
  if (configArg && configArg.type === "agencyObject") {
1866
1873
  // Extract tools from config object
1867
1874
  const toolsEntry = configArg.entries.find((e) => !("type" in e && e.type === "splat") &&
@@ -1881,6 +1888,10 @@ export class TypeScriptBuilder {
1881
1888
  configToolNames.push(toolArg.value);
1882
1889
  }
1883
1890
  }
1891
+ else if (item.type === "splat") {
1892
+ // Pass-through: spread MCP tool arrays (or any other spread) directly
1893
+ configToolExprs.push(ts.spread(this.processNode(item.value)));
1894
+ }
1884
1895
  }
1885
1896
  }
1886
1897
  // Build clientConfig without known keys
@@ -1912,12 +1923,14 @@ export class TypeScriptBuilder {
1912
1923
  const toolNodes = allToolNames.map((name) => $(ts.id("tool"))
1913
1924
  .call([ts.str(name)])
1914
1925
  .done());
1926
+ // Merge registry-resolved tools with pass-through expressions (spreads of MCP tools, etc.)
1927
+ const allToolNodes = [...toolNodes, ...configToolExprs];
1915
1928
  // Merge tools into clientConfig
1916
1929
  let mergedConfig;
1917
- if (allToolNames.length > 0) {
1930
+ if (allToolNodes.length > 0) {
1918
1931
  // Spread user config and add tools
1919
1932
  mergedConfig = ts.obj([
1920
- ts.set("tools", ts.arr(toolNodes)),
1933
+ ts.set("tools", ts.arr(allToolNodes)),
1921
1934
  ts.setSpread(clientConfig),
1922
1935
  ]);
1923
1936
  }
@@ -2418,10 +2431,14 @@ export class TypeScriptBuilder {
2418
2431
  traceConfigFields.traceFile = ts.str(this.agencyConfig.traceFile);
2419
2432
  }
2420
2433
  runtimeCtxArgs.traceConfig = ts.obj(traceConfigFields);
2421
- let runtimeCtx = ts.statements([
2434
+ const runtimeCtxStatements = [
2422
2435
  ts.constDecl("__globalCtx", ts.new(ts.id("RuntimeContext"), [ts.obj(runtimeCtxArgs)])),
2423
2436
  ts.constDecl("graph", $(ts.runtime.globalCtx).prop("graph").done()),
2424
- ]);
2437
+ ];
2438
+ if (this.agencyConfig.mcpServers) {
2439
+ runtimeCtxStatements.push(ts.raw(`__globalCtx.createMcpManager(${JSON.stringify(this.agencyConfig.mcpServers)});`));
2440
+ }
2441
+ let runtimeCtx = ts.statements(runtimeCtxStatements);
2425
2442
  return renderImports.default({
2426
2443
  runtimeContextCode: printTs(runtimeCtx),
2427
2444
  });
@@ -1,6 +1,7 @@
1
1
  import * as builtinFunctionsInput from "../../templates/backends/typescriptGenerator/builtinFunctions/input.js";
2
2
  import * as builtinFunctionsRead from "../../templates/backends/typescriptGenerator/builtinFunctions/read.js";
3
3
  import * as builtinFunctionsFetchJSON from "../../templates/backends/typescriptGenerator/builtinFunctions/fetchJSON.js";
4
+ import * as builtinFunctionsMcp from "../../templates/backends/typescriptGenerator/builtinFunctions/mcp.js";
4
5
  import { BUILTIN_FUNCTIONS } from "../../config.js";
5
6
  /**
6
7
  * Maps an Agency function name to its TypeScript equivalent
@@ -46,5 +47,7 @@ export function generateBuiltinHelpers(functionsUsed) {
46
47
  helpers.push(sleepFunc);
47
48
  }
48
49
  */
50
+ const mcpFunc = builtinFunctionsMcp.default({});
51
+ helpers.push(mcpFunc);
49
52
  return helpers.join("\n\n");
50
53
  }
@@ -1,4 +1,4 @@
1
1
  import { AgencyProgram } from "../types.js";
2
2
  import { type ProgramInfo } from "../programInfo.js";
3
3
  import { AgencyConfig } from "../config.js";
4
- export declare function generateTypeScript(program: AgencyProgram, config?: AgencyConfig, info?: ProgramInfo, moduleId?: string): string;
4
+ export declare function generateTypeScript(program: AgencyProgram, config?: AgencyConfig, info?: ProgramInfo, moduleId?: string, outputFile?: string): string;
@@ -2,14 +2,14 @@ import { TypescriptPreprocessor } from "../preprocessors/typescriptPreprocessor.
2
2
  import { collectProgramInfo } from "../programInfo.js";
3
3
  import { TypeScriptBuilder } from "./typescriptBuilder.js";
4
4
  import { printTs } from "../ir/prettyPrint.js";
5
- export function generateTypeScript(program, config, info, moduleId) {
5
+ export function generateTypeScript(program, config, info, moduleId, outputFile) {
6
6
  if (!moduleId) {
7
7
  throw new Error("moduleId is required for generateTypeScript");
8
8
  }
9
9
  const programInfo = info ?? collectProgramInfo(program);
10
10
  const preprocessor = new TypescriptPreprocessor(program, config, programInfo);
11
11
  const preprocessedProgram = preprocessor.preprocess();
12
- const builder = new TypeScriptBuilder(config, programInfo, moduleId);
12
+ const builder = new TypeScriptBuilder(config, programInfo, moduleId, outputFile);
13
13
  const ir = builder.build(preprocessedProgram);
14
14
  return printTs(ir);
15
15
  }
@@ -1,4 +1,5 @@
1
1
  import { generateAgency } from "../backends/agencyGenerator.js";
2
+ import { AgencyConfigSchema } from "../config.js";
2
3
  import { generateTypeScript } from "../index.js";
3
4
  import { resolveImports } from "../preprocessors/importResolver.js";
4
5
  import { collectProgramInfo } from "../programInfo.js";
@@ -26,6 +27,15 @@ export function loadConfig(configPath, verbose = false) {
26
27
  try {
27
28
  const configContent = fs.readFileSync(finalConfigPath, "utf-8");
28
29
  config = JSON.parse(configContent);
30
+ const parseResult = AgencyConfigSchema.safeParse(config);
31
+ if (!parseResult.success) {
32
+ console.error(`Invalid agency.json config:`);
33
+ for (const issue of parseResult.error.issues) {
34
+ console.error(` - ${issue.path.join(".")}: ${issue.message}`);
35
+ }
36
+ process.exit(1);
37
+ }
38
+ config = parseResult.data;
29
39
  if (config.verbose) {
30
40
  console.log(`Loaded config from ${finalConfigPath}`);
31
41
  }
@@ -154,9 +164,16 @@ export function compile(config, inputFile, _outputFile, options) {
154
164
  nonAgencyImports.push(node.modulePath);
155
165
  }
156
166
  });
157
- strategy.prepareDependencies(nonAgencyImports, absoluteInputFile);
167
+ try {
168
+ strategy.prepareDependencies(nonAgencyImports, absoluteInputFile);
169
+ }
170
+ catch (error) {
171
+ console.error(error instanceof Error ? error.message : String(error));
172
+ process.exit(1);
173
+ }
158
174
  const moduleId = path.relative(process.cwd(), absoluteInputFile);
159
- const generatedCode = generateTypeScript(resolvedProgram, config, info, moduleId);
175
+ const absoluteOutputFile = path.resolve(outputFile);
176
+ const generatedCode = generateTypeScript(resolvedProgram, config, info, moduleId, absoluteOutputFile);
160
177
  if (options?.ts) {
161
178
  // TypeScript output — add @ts-nocheck so type errors don't block compilation
162
179
  fs.writeFileSync(outputFile, "// @ts-nocheck\n" + generatedCode, "utf-8");
@@ -83,7 +83,14 @@ export async function debug(config, _inputFile, options = {}) {
83
83
  let absOutput;
84
84
  if (distDir) {
85
85
  // distDir mode: import pre-compiled JS from the dist directory
86
- const compiledPath = resolveCompiledFile(distDir, inputFile);
86
+ let compiledPath;
87
+ try {
88
+ compiledPath = resolveCompiledFile(distDir, inputFile);
89
+ }
90
+ catch (error) {
91
+ console.error(error instanceof Error ? error.message : String(error));
92
+ process.exit(1);
93
+ }
87
94
  // Warn if source is newer than compiled output
88
95
  const sourceMtime = fs.statSync(inputFile).mtimeMs;
89
96
  const compiledMtime = fs.statSync(compiledPath).mtimeMs;
@@ -1,4 +1,6 @@
1
1
  import { AgencyNode } from "./types.js";
2
+ import { z } from "zod";
3
+ import type { McpServerConfig } from "./runtime/mcp/types.js";
2
4
  export declare const TYPES_THAT_DONT_TRIGGER_NEW_PART: AgencyNode["type"][];
3
5
  /**
4
6
  * Maps Agency built-in function names to TypeScript equivalents.
@@ -117,4 +119,66 @@ export interface AgencyConfig {
117
119
  /** Base URL for source links in generated docs */
118
120
  baseUrl?: string;
119
121
  };
122
+ /** MCP server configurations */
123
+ mcpServers?: Record<string, McpServerConfig>;
120
124
  }
125
+ export declare const AgencyConfigSchema: z.ZodObject<{
126
+ verbose: z.ZodOptional<z.ZodBoolean>;
127
+ outDir: z.ZodOptional<z.ZodString>;
128
+ excludeNodeTypes: z.ZodOptional<z.ZodArray<z.ZodString>>;
129
+ excludeBuiltinFunctions: z.ZodOptional<z.ZodArray<z.ZodString>>;
130
+ allowedFetchDomains: z.ZodOptional<z.ZodArray<z.ZodString>>;
131
+ disallowedFetchDomains: z.ZodOptional<z.ZodArray<z.ZodString>>;
132
+ tarsecTraceHost: z.ZodOptional<z.ZodString>;
133
+ maxToolCallRounds: z.ZodOptional<z.ZodNumber>;
134
+ log: z.ZodOptional<z.ZodObject<{
135
+ host: z.ZodOptional<z.ZodString>;
136
+ projectId: z.ZodOptional<z.ZodString>;
137
+ debugMode: z.ZodOptional<z.ZodBoolean>;
138
+ apiKey: z.ZodOptional<z.ZodString>;
139
+ }, z.core.$strip>>;
140
+ client: z.ZodOptional<z.ZodObject<{
141
+ logLevel: z.ZodOptional<z.ZodEnum<{
142
+ error: "error";
143
+ warn: "warn";
144
+ info: "info";
145
+ debug: "debug";
146
+ }>>;
147
+ defaultModel: z.ZodOptional<z.ZodString>;
148
+ openAiApiKey: z.ZodOptional<z.ZodString>;
149
+ googleApiKey: z.ZodOptional<z.ZodString>;
150
+ statelog: z.ZodOptional<z.ZodObject<{
151
+ host: z.ZodOptional<z.ZodString>;
152
+ projectId: z.ZodOptional<z.ZodString>;
153
+ apiKey: z.ZodOptional<z.ZodString>;
154
+ }, z.core.$strip>>;
155
+ }, z.core.$strip>>;
156
+ strictTypes: z.ZodOptional<z.ZodBoolean>;
157
+ typeCheck: z.ZodOptional<z.ZodBoolean>;
158
+ typeCheckStrict: z.ZodOptional<z.ZodBoolean>;
159
+ restrictImports: z.ZodOptional<z.ZodBoolean>;
160
+ debugger: z.ZodOptional<z.ZodBoolean>;
161
+ instrument: z.ZodOptional<z.ZodBoolean>;
162
+ checkpoints: z.ZodOptional<z.ZodObject<{
163
+ maxRestores: z.ZodOptional<z.ZodNumber>;
164
+ }, z.core.$strip>>;
165
+ trace: z.ZodOptional<z.ZodBoolean>;
166
+ traceFile: z.ZodOptional<z.ZodString>;
167
+ traceDir: z.ZodOptional<z.ZodString>;
168
+ distDir: z.ZodOptional<z.ZodString>;
169
+ test: z.ZodOptional<z.ZodObject<{
170
+ parallel: z.ZodOptional<z.ZodNumber>;
171
+ }, z.core.$strip>>;
172
+ doc: z.ZodOptional<z.ZodObject<{
173
+ outDir: z.ZodOptional<z.ZodString>;
174
+ baseUrl: z.ZodOptional<z.ZodString>;
175
+ }, z.core.$strip>>;
176
+ mcpServers: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodObject<{
177
+ command: z.ZodString;
178
+ args: z.ZodOptional<z.ZodArray<z.ZodString>>;
179
+ env: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
180
+ }, z.core.$strict>, z.ZodObject<{
181
+ type: z.ZodLiteral<"http">;
182
+ url: z.ZodString;
183
+ }, z.core.$strict>]>>>;
184
+ }, z.core.$loose>;
@@ -1,3 +1,4 @@
1
+ import { z } from "zod";
1
2
  export const TYPES_THAT_DONT_TRIGGER_NEW_PART = [
2
3
  "typeAlias",
3
4
  "usesTool",
@@ -16,3 +17,46 @@ export const BUILTIN_TOOLS = [
16
17
  "readSkill",
17
18
  ];
18
19
  export const BUILTIN_VARIABLES = ["color"];
20
+ // --- Zod schema for runtime validation of agency.json ---
21
+ const McpStdioServerSchema = z.object({
22
+ command: z.string(),
23
+ args: z.array(z.string()).optional(),
24
+ env: z.record(z.string(), z.string()).optional(),
25
+ }).strict();
26
+ const McpHttpServerSchema = z.object({
27
+ type: z.literal("http"),
28
+ url: z.string(),
29
+ }).strict();
30
+ const McpServerSchema = z.union([McpStdioServerSchema, McpHttpServerSchema]);
31
+ export const AgencyConfigSchema = z.object({
32
+ verbose: z.boolean(),
33
+ outDir: z.string(),
34
+ excludeNodeTypes: z.array(z.string()),
35
+ excludeBuiltinFunctions: z.array(z.string()),
36
+ allowedFetchDomains: z.array(z.string()),
37
+ disallowedFetchDomains: z.array(z.string()),
38
+ tarsecTraceHost: z.string(),
39
+ maxToolCallRounds: z.number(),
40
+ log: z.object({ host: z.string(), projectId: z.string(), debugMode: z.boolean(), apiKey: z.string() }).partial(),
41
+ client: z.object({
42
+ logLevel: z.enum(["error", "warn", "info", "debug"]),
43
+ defaultModel: z.string(),
44
+ openAiApiKey: z.string(),
45
+ googleApiKey: z.string(),
46
+ statelog: z.object({ host: z.string(), projectId: z.string(), apiKey: z.string() }).partial(),
47
+ }).partial(),
48
+ strictTypes: z.boolean(),
49
+ typeCheck: z.boolean(),
50
+ typeCheckStrict: z.boolean(),
51
+ restrictImports: z.boolean(),
52
+ debugger: z.boolean(),
53
+ instrument: z.boolean(),
54
+ checkpoints: z.object({ maxRestores: z.number() }).partial(),
55
+ trace: z.boolean(),
56
+ traceFile: z.string(),
57
+ traceDir: z.string(),
58
+ distDir: z.string(),
59
+ test: z.object({ parallel: z.number() }).partial(),
60
+ doc: z.object({ outDir: z.string(), baseUrl: z.string() }).partial(),
61
+ mcpServers: z.record(z.string(), McpServerSchema),
62
+ }).partial().passthrough();
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,64 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { AgencyConfigSchema } from "./config.js";
3
+ describe("AgencyConfigSchema", () => {
4
+ it("should accept an empty config", () => {
5
+ const result = AgencyConfigSchema.safeParse({});
6
+ expect(result.success).toBe(true);
7
+ });
8
+ it("should accept a config with existing fields", () => {
9
+ const result = AgencyConfigSchema.safeParse({
10
+ verbose: true,
11
+ outDir: "dist",
12
+ maxToolCallRounds: 5,
13
+ });
14
+ expect(result.success).toBe(true);
15
+ });
16
+ it("should accept a config with stdio MCP server", () => {
17
+ const result = AgencyConfigSchema.safeParse({
18
+ mcpServers: {
19
+ filesystem: {
20
+ command: "npx",
21
+ args: ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"],
22
+ env: { FOO: "bar" },
23
+ },
24
+ },
25
+ });
26
+ expect(result.success).toBe(true);
27
+ });
28
+ it("should accept a config with HTTP MCP server", () => {
29
+ const result = AgencyConfigSchema.safeParse({
30
+ mcpServers: {
31
+ weather: {
32
+ type: "http",
33
+ url: "https://weather-mcp.example.com/mcp",
34
+ },
35
+ },
36
+ });
37
+ expect(result.success).toBe(true);
38
+ });
39
+ it("should accept a config with mixed MCP servers", () => {
40
+ const result = AgencyConfigSchema.safeParse({
41
+ mcpServers: {
42
+ filesystem: { command: "npx", args: ["server"] },
43
+ weather: { type: "http", url: "https://example.com/mcp" },
44
+ },
45
+ });
46
+ expect(result.success).toBe(true);
47
+ });
48
+ it("should reject an HTTP server missing url", () => {
49
+ const result = AgencyConfigSchema.safeParse({
50
+ mcpServers: {
51
+ bad: { type: "http" },
52
+ },
53
+ });
54
+ expect(result.success).toBe(false);
55
+ });
56
+ it("should reject a stdio server missing command", () => {
57
+ const result = AgencyConfigSchema.safeParse({
58
+ mcpServers: {
59
+ bad: { args: ["foo"] },
60
+ },
61
+ });
62
+ expect(result.success).toBe(false);
63
+ });
64
+ });
@@ -46,10 +46,11 @@ export class UIState {
46
46
  // If the moduleId refers to a stdlib file, resolve against the actual
47
47
  // stdlib directory (handles npm-installed packages where stdlib lives
48
48
  // in node_modules/agency-lang/stdlib/)
49
- if (moduleId.includes("stdlib")) {
50
- const basename = path.basename(moduleId);
49
+ const stdlibMatch = moduleId.match(/(?:^|[/\\])stdlib[/\\](.+)$/);
50
+ if (stdlibMatch) {
51
+ const stdlibRelativePath = stdlibMatch[1];
51
52
  for (const ext of extensions) {
52
- const candidate = path.join(getStdlibDir(), basename).replace(/\.agency$/, ext);
53
+ const candidate = path.join(getStdlibDir(), stdlibRelativePath).replace(/\.agency$/, ext);
53
54
  if (fs.existsSync(candidate))
54
55
  return candidate;
55
56
  }
@@ -68,7 +68,11 @@ export declare function resolveAgencyImportPath(importPath: string, fromFile: st
68
68
  * Convert an Agency import path to the path that should appear in generated
69
69
  * TypeScript import statements.
70
70
  *
71
- * - "std::foo" -> absolute path to <stdlib-dir>/foo.js
71
+ * - "std::foo" -> relative path to <stdlib-dir>/foo.js from the source file
72
72
  * - "./foo.agency" -> "./foo.js" (relative, just extension swap)
73
+ *
74
+ * @param fromFile - Absolute path of the source file containing the import.
75
+ * Used to compute relative paths for stdlib imports. If not provided,
76
+ * falls back to absolute paths.
73
77
  */
74
- export declare function toCompiledImportPath(importPath: string): string;
78
+ export declare function toCompiledImportPath(importPath: string, fromFile?: string): string;
@@ -220,12 +220,23 @@ export function resolveAgencyImportPath(importPath, fromFile) {
220
220
  * Convert an Agency import path to the path that should appear in generated
221
221
  * TypeScript import statements.
222
222
  *
223
- * - "std::foo" -> absolute path to <stdlib-dir>/foo.js
223
+ * - "std::foo" -> relative path to <stdlib-dir>/foo.js from the source file
224
224
  * - "./foo.agency" -> "./foo.js" (relative, just extension swap)
225
+ *
226
+ * @param fromFile - Absolute path of the source file containing the import.
227
+ * Used to compute relative paths for stdlib imports. If not provided,
228
+ * falls back to absolute paths.
225
229
  */
226
- export function toCompiledImportPath(importPath) {
230
+ export function toCompiledImportPath(importPath, fromFile) {
227
231
  if (isStdlibImport(importPath)) {
228
- return path.join(STDLIB_DIR, normalizeStdlibPath(importPath) + ".js");
232
+ const absTarget = path.join(STDLIB_DIR, normalizeStdlibPath(importPath) + ".js");
233
+ if (fromFile) {
234
+ let rel = path.relative(path.dirname(fromFile), absTarget).replace(/\\/g, "/");
235
+ if (!rel.startsWith("."))
236
+ rel = "./" + rel;
237
+ return rel;
238
+ }
239
+ return absTarget;
229
240
  }
230
241
  if (isPkgImport(importPath)) {
231
242
  // Emit bare specifier — Node resolves it at runtime via node_modules
@@ -34,4 +34,7 @@ export declare class RunStrategy extends CompileStrategy {
34
34
  constructor();
35
35
  rewriteImport(modulePath: string, sourceFile: string): string;
36
36
  prepareDependencies(imports: string[], sourceFile: string): void;
37
+ private ensureJsExists;
38
+ /** Extract relative .js imports from TypeScript source code. */
39
+ private getLocalJsImports;
37
40
  }
@@ -37,28 +37,53 @@ export class RunStrategy extends CompileStrategy {
37
37
  return modulePath.replace(/\.ts$/, ".js");
38
38
  }
39
39
  prepareDependencies(imports, sourceFile) {
40
+ const visited = new Set();
40
41
  for (const imp of imports) {
41
42
  if (!imp.startsWith("./") && !imp.startsWith("../"))
42
43
  continue;
43
44
  if (!imp.endsWith(".js"))
44
45
  continue;
45
46
  const resolved = path.resolve(path.dirname(sourceFile), imp);
46
- if (fs.existsSync(resolved))
47
- continue;
48
- const tsPath = resolved.replace(/\.js$/, ".ts");
49
- if (fs.existsSync(tsPath)) {
50
- const tsCode = fs.readFileSync(tsPath, "utf-8");
51
- const result = transformSync(tsCode, {
52
- loader: "ts",
53
- format: "esm",
54
- supported: { "top-level-await": true },
55
- });
56
- fs.writeFileSync(resolved, result.code);
57
- }
58
- else {
59
- throw new Error(`Cannot resolve import '${imp}' from '${sourceFile}'.\n` +
60
- `Tried: ${resolved}, ${tsPath} — neither file exists.`);
47
+ this.ensureJsExists(resolved, sourceFile, visited);
48
+ }
49
+ }
50
+ ensureJsExists(jsPath, importer, visited) {
51
+ const normalized = path.normalize(jsPath);
52
+ if (visited.has(normalized))
53
+ return;
54
+ visited.add(normalized);
55
+ if (fs.existsSync(normalized))
56
+ return;
57
+ const tsPath = normalized.replace(/\.js$/, ".ts");
58
+ if (!fs.existsSync(tsPath)) {
59
+ throw new Error(`Cannot resolve import '${path.relative(path.dirname(importer), normalized)}' from '${importer}'.\n` +
60
+ `Tried: ${normalized}, ${tsPath} — neither file exists.`);
61
+ }
62
+ // Recursively ensure this file's dependencies exist first
63
+ const tsCode = fs.readFileSync(tsPath, "utf-8");
64
+ for (const nestedImp of this.getLocalJsImports(tsCode)) {
65
+ const nestedResolved = path.resolve(path.dirname(tsPath), nestedImp);
66
+ this.ensureJsExists(nestedResolved, tsPath, visited);
67
+ }
68
+ // Then compile this file
69
+ const result = transformSync(tsCode, {
70
+ loader: "ts",
71
+ format: "esm",
72
+ supported: { "top-level-await": true },
73
+ });
74
+ fs.writeFileSync(normalized, result.code);
75
+ }
76
+ /** Extract relative .js imports from TypeScript source code. */
77
+ getLocalJsImports(code) {
78
+ const imports = [];
79
+ const pattern = /\bimport\s+(?:[^'"]+?\s+from\s+)?["']([^"']+)["']/g;
80
+ let match;
81
+ while ((match = pattern.exec(code)) !== null) {
82
+ const specifier = match[1];
83
+ if ((specifier.startsWith("./") || specifier.startsWith("../")) && specifier.endsWith(".js")) {
84
+ imports.push(specifier);
61
85
  }
62
86
  }
87
+ return imports;
63
88
  }
64
89
  }
@@ -35,3 +35,5 @@ export { DebuggerState } from "../debugger/debuggerState.js";
35
35
  export { success, failure, isSuccess, isFailure, __pipeBind, __tryCall, __catchResult, } from "./result.js";
36
36
  export type { ResultValue, ResultSuccess, ResultFailure } from "./result.js";
37
37
  export { Schema, __validateType } from "./schema.js";
38
+ export { McpManager } from "./mcp/mcpManager.js";
39
+ export type { McpServerConfig, McpTool } from "./mcp/types.js";
@@ -24,3 +24,4 @@ export { debugStep } from "./debugger.js";
24
24
  export { DebuggerState } from "../debugger/debuggerState.js";
25
25
  export { success, failure, isSuccess, isFailure, __pipeBind, __tryCall, __catchResult, } from "./result.js";
26
26
  export { Schema, __validateType } from "./schema.js";
27
+ export { McpManager } from "./mcp/mcpManager.js";
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,14 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
+ import { z } from "zod";
4
+ const server = new McpServer({
5
+ name: "test-server",
6
+ version: "1.0.0",
7
+ });
8
+ server.tool("add", "Add two numbers together", { a: z.number(), b: z.number() }, async ({ a, b }) => {
9
+ return {
10
+ content: [{ type: "text", text: String(a + b) }],
11
+ };
12
+ });
13
+ const transport = new StdioServerTransport();
14
+ await server.connect(transport);
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,37 @@
1
+ import { describe, it, expect, afterEach } from "vitest";
2
+ import { McpManager } from "./mcpManager.js";
3
+ import { mcpToolToRegistryEntry } from "./toolAdapter.js";
4
+ import path from "path";
5
+ import { fileURLToPath } from "url";
6
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
7
+ const TEST_SERVER_PATH = path.join(__dirname, "__tests__", "testServer.ts");
8
+ describe("MCP integration", () => {
9
+ let manager;
10
+ afterEach(async () => {
11
+ if (manager) {
12
+ await manager.disconnectAll();
13
+ }
14
+ });
15
+ it("tools survive JSON serialization round-trip and remain callable", async () => {
16
+ manager = new McpManager({
17
+ test: {
18
+ command: "npx",
19
+ args: ["tsx", TEST_SERVER_PATH],
20
+ },
21
+ });
22
+ const result = await manager.getTools("test");
23
+ expect(result.success).toBe(true);
24
+ if (!result.success)
25
+ throw new Error("unreachable");
26
+ const tools = result.value;
27
+ const serialized = JSON.stringify(tools);
28
+ const deserialized = JSON.parse(serialized);
29
+ expect(deserialized[0].__mcpTool).toBe(true);
30
+ expect(deserialized[0].serverName).toBe("test");
31
+ expect(deserialized[0].name).toBe("test__add");
32
+ // After deserialization, we can still build a working handler
33
+ const entry = mcpToolToRegistryEntry(deserialized[0], (serverName, toolName, args) => manager.callTool(serverName, toolName, args));
34
+ const toolResult = await entry.handler.execute(3, 4, { ctx: null });
35
+ expect(toolResult).toContain("7");
36
+ });
37
+ });
@@ -0,0 +1,14 @@
1
+ import type { McpServerConfig, McpTool } from "./types.js";
2
+ export declare class McpConnection {
3
+ private client;
4
+ private serverName;
5
+ private config;
6
+ private tools;
7
+ private connected;
8
+ constructor(serverName: string, config: McpServerConfig);
9
+ connect(): Promise<void>;
10
+ getTools(): McpTool[];
11
+ callTool(toolName: string, args: Record<string, unknown>): Promise<string>;
12
+ disconnect(): Promise<void>;
13
+ isConnected(): boolean;
14
+ }