agency-lang 0.0.101 → 0.0.103
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.
- package/dist/lib/backends/typescriptBuilder.js +16 -1
- package/dist/lib/cli/auth.d.ts +4 -0
- package/dist/lib/cli/auth.js +60 -0
- package/dist/lib/cli/commands.js +4 -0
- package/dist/lib/config.d.ts +5 -0
- package/dist/lib/config.js +63 -2
- package/dist/lib/config.test.js +114 -0
- package/dist/lib/importPaths.js +1 -8
- package/dist/lib/importPaths.test.js +3 -13
- package/dist/lib/runtime/hooks.d.ts +6 -0
- package/dist/lib/runtime/mcp/callbackServer.d.ts +20 -0
- package/dist/lib/runtime/mcp/callbackServer.js +86 -0
- package/dist/lib/runtime/mcp/callbackServer.test.d.ts +1 -0
- package/dist/lib/runtime/mcp/callbackServer.test.js +57 -0
- package/dist/lib/runtime/mcp/mcpConnection.d.ts +10 -1
- package/dist/lib/runtime/mcp/mcpConnection.js +21 -11
- package/dist/lib/runtime/mcp/mcpConnection.test.js +23 -0
- package/dist/lib/runtime/mcp/mcpManager.d.ts +10 -1
- package/dist/lib/runtime/mcp/mcpManager.js +54 -14
- package/dist/lib/runtime/mcp/mcpManager.test.js +58 -0
- package/dist/lib/runtime/mcp/oauthConnector.d.ts +41 -0
- package/dist/lib/runtime/mcp/oauthConnector.js +81 -0
- package/dist/lib/runtime/mcp/oauthConnector.test.d.ts +1 -0
- package/dist/lib/runtime/mcp/oauthConnector.test.js +26 -0
- package/dist/lib/runtime/mcp/oauthProvider.d.ts +52 -0
- package/dist/lib/runtime/mcp/oauthProvider.js +157 -0
- package/dist/lib/runtime/mcp/oauthProvider.test.d.ts +1 -0
- package/dist/lib/runtime/mcp/oauthProvider.test.js +151 -0
- package/dist/lib/runtime/mcp/tokenStore.d.ts +22 -0
- package/dist/lib/runtime/mcp/tokenStore.js +103 -0
- package/dist/lib/runtime/mcp/tokenStore.test.d.ts +1 -0
- package/dist/lib/runtime/mcp/tokenStore.test.js +80 -0
- package/dist/lib/runtime/mcp/types.d.ts +8 -0
- package/dist/lib/runtime/mcp/types.js +9 -1
- package/dist/lib/runtime/state/context.js +2 -1
- package/dist/lib/types/function.d.ts +1 -1
- package/dist/lib/types/function.js +1 -0
- package/dist/lib/version.d.ts +1 -1
- package/dist/lib/version.js +1 -1
- package/dist/scripts/agency.js +22 -0
- package/package.json +3 -2
- package/stdlib/agent.js +1 -1
- package/stdlib/array.js +1815 -1732
- package/stdlib/clipboard.js +1 -1
- package/stdlib/fs.js +1 -1
- package/stdlib/http.js +1 -1
- package/stdlib/index.js +1878 -1860
- package/stdlib/lib/system.js +17 -1
- package/stdlib/math.js +1 -1
- package/stdlib/object.js +839 -1160
- package/stdlib/path.js +1 -1
- package/stdlib/shell.js +1 -1
- package/stdlib/speech.js +1 -1
- package/stdlib/strategy.js +1 -1
- package/stdlib/system.agency +8 -1
- package/stdlib/system.js +110 -3
- package/stdlib/ui.js +1 -1
- package/stdlib/weather.js +1 -1
- package/stdlib/wikipedia.js +1 -1
- package/stdlib/_builtins.js +0 -134
- package/stdlib/_math.js +0 -9
- package/stdlib/_utils.js +0 -51
|
@@ -1641,6 +1641,12 @@ export class TypeScriptBuilder {
|
|
|
1641
1641
|
})),
|
|
1642
1642
|
]), ts.statements([
|
|
1643
1643
|
ts.if(ts.raw("__error instanceof RestoreSignal"), ts.statements([ts.throw("__error")])),
|
|
1644
|
+
ts.consoleError(ts.template([
|
|
1645
|
+
{
|
|
1646
|
+
text: "\\nAgent crashed: ",
|
|
1647
|
+
expr: $(ts.id("__error")).prop("message").done(),
|
|
1648
|
+
},
|
|
1649
|
+
])),
|
|
1644
1650
|
ts.return(ts.obj({
|
|
1645
1651
|
messages: ts.runtime.threads,
|
|
1646
1652
|
data: ts.raw(`failure(__error instanceof Error ? __error.message : String(__error), { functionName: ${JSON.stringify(nodeName)} })`),
|
|
@@ -2436,7 +2442,16 @@ export class TypeScriptBuilder {
|
|
|
2436
2442
|
ts.constDecl("graph", $(ts.runtime.globalCtx).prop("graph").done()),
|
|
2437
2443
|
];
|
|
2438
2444
|
if (this.agencyConfig.mcpServers) {
|
|
2439
|
-
|
|
2445
|
+
// Strip secrets from the config before embedding in generated code.
|
|
2446
|
+
// clientId and clientSecret are resolved at runtime from env vars.
|
|
2447
|
+
const sanitizedServers = Object.fromEntries(Object.entries(this.agencyConfig.mcpServers).map(([name, cfg]) => {
|
|
2448
|
+
if ("type" in cfg && cfg.type === "http") {
|
|
2449
|
+
const { clientSecret, clientId, ...rest } = cfg;
|
|
2450
|
+
return [name, rest];
|
|
2451
|
+
}
|
|
2452
|
+
return [name, cfg];
|
|
2453
|
+
}));
|
|
2454
|
+
runtimeCtxStatements.push(ts.raw(`__globalCtx.createMcpManager(${JSON.stringify(sanitizedServers)});`));
|
|
2440
2455
|
}
|
|
2441
2456
|
let runtimeCtx = ts.statements(runtimeCtxStatements);
|
|
2442
2457
|
return renderImports.default({
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { TokenStore } from "../runtime/mcp/tokenStore.js";
|
|
2
|
+
import { OAuthConnector } from "../runtime/mcp/oauthConnector.js";
|
|
3
|
+
import { isOAuthServer } from "../runtime/mcp/types.js";
|
|
4
|
+
const store = new TokenStore();
|
|
5
|
+
export async function authServer(serverName, config) {
|
|
6
|
+
const mcpServers = config.mcpServers;
|
|
7
|
+
if (!mcpServers || !mcpServers[serverName]) {
|
|
8
|
+
console.error(`MCP server "${serverName}" not found in agency.json. Available servers: ${mcpServers ? Object.keys(mcpServers).join(", ") : "(none)"}`);
|
|
9
|
+
process.exit(1);
|
|
10
|
+
}
|
|
11
|
+
const serverConfig = mcpServers[serverName];
|
|
12
|
+
if (!isOAuthServer(serverConfig)) {
|
|
13
|
+
console.error(`MCP server "${serverName}" is not configured with auth: "oauth".`);
|
|
14
|
+
process.exit(1);
|
|
15
|
+
}
|
|
16
|
+
const httpConfig = serverConfig;
|
|
17
|
+
const existing = await store.loadTokens(serverName);
|
|
18
|
+
if (existing) {
|
|
19
|
+
console.log(`Token already exists for "${serverName}". Use --revoke to remove it first.`);
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
console.log(`Starting OAuth authorization for "${serverName}"...`);
|
|
23
|
+
const connector = new OAuthConnector(serverName, httpConfig.url, store, {
|
|
24
|
+
timeoutMs: httpConfig.authTimeout,
|
|
25
|
+
clientId: httpConfig.clientId,
|
|
26
|
+
clientSecret: httpConfig.clientSecret,
|
|
27
|
+
});
|
|
28
|
+
try {
|
|
29
|
+
const { client } = await connector.connect();
|
|
30
|
+
console.log(`Successfully authorized "${serverName}". Token stored.`);
|
|
31
|
+
await client.close();
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
35
|
+
console.error(`Authorization failed for "${serverName}": ${msg}`);
|
|
36
|
+
process.exit(1);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
export async function listAuth() {
|
|
40
|
+
const servers = await store.listServers();
|
|
41
|
+
if (servers.length === 0) {
|
|
42
|
+
console.log("No stored OAuth tokens.");
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
console.log("Stored OAuth tokens:");
|
|
46
|
+
for (const name of servers) {
|
|
47
|
+
const tokens = await store.loadTokens(name);
|
|
48
|
+
const hasRefresh = tokens?.refresh_token ? "yes" : "no";
|
|
49
|
+
console.log(` ${name} (refresh token: ${hasRefresh})`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
export async function revokeAuth(serverName) {
|
|
53
|
+
const tokens = await store.loadTokens(serverName);
|
|
54
|
+
if (!tokens) {
|
|
55
|
+
console.log(`No stored token for "${serverName}".`);
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
await store.deleteTokens(serverName);
|
|
59
|
+
console.log(`Removed stored token for "${serverName}".`);
|
|
60
|
+
}
|
package/dist/lib/cli/commands.js
CHANGED
|
@@ -89,6 +89,10 @@ export function resetCompilationCache() {
|
|
|
89
89
|
compiledFiles.clear();
|
|
90
90
|
}
|
|
91
91
|
export function compile(config, inputFile, _outputFile, options) {
|
|
92
|
+
if (!fs.existsSync(inputFile)) {
|
|
93
|
+
console.error(`Error: Input file '${inputFile}' not found`);
|
|
94
|
+
process.exit(1);
|
|
95
|
+
}
|
|
92
96
|
// Check if the input is a directory
|
|
93
97
|
const stats = fs.statSync(inputFile);
|
|
94
98
|
const verbose = config.verbose ?? false;
|
package/dist/lib/config.d.ts
CHANGED
|
@@ -180,5 +180,10 @@ export declare const AgencyConfigSchema: z.ZodObject<{
|
|
|
180
180
|
}, z.core.$strict>, z.ZodObject<{
|
|
181
181
|
type: z.ZodLiteral<"http">;
|
|
182
182
|
url: z.ZodString;
|
|
183
|
+
auth: z.ZodOptional<z.ZodLiteral<"oauth">>;
|
|
184
|
+
authTimeout: z.ZodOptional<z.ZodNumber>;
|
|
185
|
+
clientId: z.ZodOptional<z.ZodString>;
|
|
186
|
+
clientSecret: z.ZodOptional<z.ZodString>;
|
|
187
|
+
headers: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
183
188
|
}, z.core.$strict>]>>>;
|
|
184
189
|
}, z.core.$loose>;
|
package/dist/lib/config.js
CHANGED
|
@@ -26,6 +26,11 @@ const McpStdioServerSchema = z.object({
|
|
|
26
26
|
const McpHttpServerSchema = z.object({
|
|
27
27
|
type: z.literal("http"),
|
|
28
28
|
url: z.string(),
|
|
29
|
+
auth: z.literal("oauth").optional(),
|
|
30
|
+
authTimeout: z.number().optional(),
|
|
31
|
+
clientId: z.string().optional(),
|
|
32
|
+
clientSecret: z.string().optional(),
|
|
33
|
+
headers: z.record(z.string(), z.string()).optional(),
|
|
29
34
|
}).strict();
|
|
30
35
|
const McpServerSchema = z.union([McpStdioServerSchema, McpHttpServerSchema]);
|
|
31
36
|
export const AgencyConfigSchema = z.object({
|
|
@@ -58,5 +63,61 @@ export const AgencyConfigSchema = z.object({
|
|
|
58
63
|
distDir: z.string(),
|
|
59
64
|
test: z.object({ parallel: z.number() }).partial(),
|
|
60
65
|
doc: z.object({ outDir: z.string(), baseUrl: z.string() }).partial(),
|
|
61
|
-
mcpServers: z.record(z.string(), McpServerSchema),
|
|
62
|
-
}).partial().passthrough()
|
|
66
|
+
mcpServers: z.record(z.string().regex(/^[A-Za-z0-9_-]+$/, "MCP server names must contain only letters, numbers, hyphens, and underscores"), McpServerSchema),
|
|
67
|
+
}).partial().passthrough().superRefine((data, ctx) => {
|
|
68
|
+
if (!data.mcpServers)
|
|
69
|
+
return;
|
|
70
|
+
for (const [name, server] of Object.entries(data.mcpServers)) {
|
|
71
|
+
if ("type" in server && server.type === "http") {
|
|
72
|
+
const httpServer = server;
|
|
73
|
+
if (httpServer.auth && httpServer.headers) {
|
|
74
|
+
ctx.addIssue({
|
|
75
|
+
code: z.ZodIssueCode.custom,
|
|
76
|
+
message: `MCP server "${name}": cannot specify both 'auth' and 'headers'`,
|
|
77
|
+
path: ["mcpServers", name],
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
if (httpServer.authTimeout && httpServer.auth !== "oauth") {
|
|
81
|
+
ctx.addIssue({
|
|
82
|
+
code: z.ZodIssueCode.custom,
|
|
83
|
+
message: `MCP server "${name}": 'authTimeout' requires 'auth: "oauth"'`,
|
|
84
|
+
path: ["mcpServers", name],
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
if (httpServer.clientId && httpServer.auth !== "oauth") {
|
|
88
|
+
ctx.addIssue({
|
|
89
|
+
code: z.ZodIssueCode.custom,
|
|
90
|
+
message: `MCP server "${name}": 'clientId' requires 'auth: "oauth"'`,
|
|
91
|
+
path: ["mcpServers", name],
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
if (httpServer.clientSecret && httpServer.auth !== "oauth") {
|
|
95
|
+
ctx.addIssue({
|
|
96
|
+
code: z.ZodIssueCode.custom,
|
|
97
|
+
message: `MCP server "${name}": 'clientSecret' requires 'auth: "oauth"'`,
|
|
98
|
+
path: ["mcpServers", name],
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
if (httpServer.auth === "oauth") {
|
|
102
|
+
try {
|
|
103
|
+
const parsed = new URL(httpServer.url);
|
|
104
|
+
const isLocalhost = ["127.0.0.1", "localhost"].includes(parsed.hostname);
|
|
105
|
+
if (parsed.protocol !== "https:" && !isLocalhost) {
|
|
106
|
+
ctx.addIssue({
|
|
107
|
+
code: z.ZodIssueCode.custom,
|
|
108
|
+
message: `MCP server "${name}": OAuth requires HTTPS (or localhost for development)`,
|
|
109
|
+
path: ["mcpServers", name, "url"],
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
ctx.addIssue({
|
|
115
|
+
code: z.ZodIssueCode.custom,
|
|
116
|
+
message: `MCP server "${name}": invalid URL "${httpServer.url}"`,
|
|
117
|
+
path: ["mcpServers", name, "url"],
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
});
|
package/dist/lib/config.test.js
CHANGED
|
@@ -61,4 +61,118 @@ describe("AgencyConfigSchema", () => {
|
|
|
61
61
|
});
|
|
62
62
|
expect(result.success).toBe(false);
|
|
63
63
|
});
|
|
64
|
+
it("should accept HTTP server with auth: oauth", () => {
|
|
65
|
+
const result = AgencyConfigSchema.safeParse({
|
|
66
|
+
mcpServers: {
|
|
67
|
+
github: { type: "http", url: "https://github-mcp.example.com/mcp", auth: "oauth" },
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
expect(result.success).toBe(true);
|
|
71
|
+
});
|
|
72
|
+
it("should accept HTTP server with headers", () => {
|
|
73
|
+
const result = AgencyConfigSchema.safeParse({
|
|
74
|
+
mcpServers: {
|
|
75
|
+
weather: {
|
|
76
|
+
type: "http",
|
|
77
|
+
url: "https://weather.example.com/mcp",
|
|
78
|
+
headers: { "Authorization": "Bearer ${WEATHER_KEY}" },
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
});
|
|
82
|
+
expect(result.success).toBe(true);
|
|
83
|
+
});
|
|
84
|
+
it("should accept HTTP server with authTimeout", () => {
|
|
85
|
+
const result = AgencyConfigSchema.safeParse({
|
|
86
|
+
mcpServers: {
|
|
87
|
+
github: {
|
|
88
|
+
type: "http",
|
|
89
|
+
url: "https://github-mcp.example.com/mcp",
|
|
90
|
+
auth: "oauth",
|
|
91
|
+
authTimeout: 120000,
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
});
|
|
95
|
+
expect(result.success).toBe(true);
|
|
96
|
+
});
|
|
97
|
+
it("should reject HTTP server with both auth and headers", () => {
|
|
98
|
+
const result = AgencyConfigSchema.safeParse({
|
|
99
|
+
mcpServers: {
|
|
100
|
+
github: {
|
|
101
|
+
type: "http",
|
|
102
|
+
url: "https://example.com/mcp",
|
|
103
|
+
auth: "oauth",
|
|
104
|
+
headers: { "Authorization": "Bearer token" },
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
});
|
|
108
|
+
expect(result.success).toBe(false);
|
|
109
|
+
});
|
|
110
|
+
it("should reject authTimeout without auth: oauth", () => {
|
|
111
|
+
const result = AgencyConfigSchema.safeParse({
|
|
112
|
+
mcpServers: {
|
|
113
|
+
github: {
|
|
114
|
+
type: "http",
|
|
115
|
+
url: "https://example.com/mcp",
|
|
116
|
+
authTimeout: 120000,
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
});
|
|
120
|
+
expect(result.success).toBe(false);
|
|
121
|
+
});
|
|
122
|
+
it("should reject clientSecret without auth: oauth", () => {
|
|
123
|
+
const result = AgencyConfigSchema.safeParse({
|
|
124
|
+
mcpServers: {
|
|
125
|
+
github: {
|
|
126
|
+
type: "http",
|
|
127
|
+
url: "https://example.com/mcp",
|
|
128
|
+
clientSecret: "secret",
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
});
|
|
132
|
+
expect(result.success).toBe(false);
|
|
133
|
+
});
|
|
134
|
+
it("should reject OAuth over plain HTTP (non-localhost)", () => {
|
|
135
|
+
const result = AgencyConfigSchema.safeParse({
|
|
136
|
+
mcpServers: {
|
|
137
|
+
github: {
|
|
138
|
+
type: "http",
|
|
139
|
+
url: "http://evil.com/mcp",
|
|
140
|
+
auth: "oauth",
|
|
141
|
+
},
|
|
142
|
+
},
|
|
143
|
+
});
|
|
144
|
+
expect(result.success).toBe(false);
|
|
145
|
+
});
|
|
146
|
+
it("should allow OAuth over HTTP on localhost", () => {
|
|
147
|
+
const result = AgencyConfigSchema.safeParse({
|
|
148
|
+
mcpServers: {
|
|
149
|
+
local: {
|
|
150
|
+
type: "http",
|
|
151
|
+
url: "http://localhost:3000/mcp",
|
|
152
|
+
auth: "oauth",
|
|
153
|
+
},
|
|
154
|
+
},
|
|
155
|
+
});
|
|
156
|
+
expect(result.success).toBe(true);
|
|
157
|
+
});
|
|
158
|
+
it("should reject server names with invalid characters", () => {
|
|
159
|
+
const result = AgencyConfigSchema.safeParse({
|
|
160
|
+
mcpServers: {
|
|
161
|
+
"../evil": { command: "npx", args: ["server"] },
|
|
162
|
+
},
|
|
163
|
+
});
|
|
164
|
+
expect(result.success).toBe(false);
|
|
165
|
+
});
|
|
166
|
+
it("should reject auth on stdio server", () => {
|
|
167
|
+
const result = AgencyConfigSchema.safeParse({
|
|
168
|
+
mcpServers: {
|
|
169
|
+
local: {
|
|
170
|
+
command: "npx",
|
|
171
|
+
args: ["some-server"],
|
|
172
|
+
auth: "oauth",
|
|
173
|
+
},
|
|
174
|
+
},
|
|
175
|
+
});
|
|
176
|
+
expect(result.success).toBe(false);
|
|
177
|
+
});
|
|
64
178
|
});
|
package/dist/lib/importPaths.js
CHANGED
|
@@ -229,14 +229,7 @@ export function resolveAgencyImportPath(importPath, fromFile) {
|
|
|
229
229
|
*/
|
|
230
230
|
export function toCompiledImportPath(importPath, fromFile) {
|
|
231
231
|
if (isStdlibImport(importPath)) {
|
|
232
|
-
|
|
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;
|
|
232
|
+
return "agency-lang/stdlib/" + normalizeStdlibPath(importPath) + ".js";
|
|
240
233
|
}
|
|
241
234
|
if (isPkgImport(importPath)) {
|
|
242
235
|
// Emit bare specifier — Node resolves it at runtime via node_modules
|
|
@@ -35,9 +35,9 @@ describe("resolveAgencyImportPath", () => {
|
|
|
35
35
|
});
|
|
36
36
|
});
|
|
37
37
|
describe("toCompiledImportPath", () => {
|
|
38
|
-
it("should convert std:: paths to absolute
|
|
38
|
+
it("should convert std:: paths to package-absolute paths", () => {
|
|
39
39
|
const result = toCompiledImportPath("std::math");
|
|
40
|
-
expect(result).toBe(
|
|
40
|
+
expect(result).toBe("agency-lang/stdlib/math.js");
|
|
41
41
|
});
|
|
42
42
|
it("should convert relative .agency paths to .js", () => {
|
|
43
43
|
const result = toCompiledImportPath("./utils.agency");
|
|
@@ -45,17 +45,7 @@ describe("toCompiledImportPath", () => {
|
|
|
45
45
|
});
|
|
46
46
|
it("should handle std:: paths with subdirectories", () => {
|
|
47
47
|
const result = toCompiledImportPath("std::collections/queue");
|
|
48
|
-
expect(result).toBe(
|
|
49
|
-
});
|
|
50
|
-
});
|
|
51
|
-
describe("std:: import code generation", () => {
|
|
52
|
-
it("generated import path should point to a .js file inside the stdlib dir", () => {
|
|
53
|
-
const result = toCompiledImportPath("std::math");
|
|
54
|
-
// Must be an absolute path ending with stdlib/math.js
|
|
55
|
-
expect(path.isAbsolute(result)).toBe(true);
|
|
56
|
-
expect(result).toMatch(/stdlib[/\\]math\.js$/);
|
|
57
|
-
// The stdlib dir should exist
|
|
58
|
-
expect(fs.existsSync(path.dirname(result))).toBe(true);
|
|
48
|
+
expect(result).toBe("agency-lang/stdlib/collections/queue.js");
|
|
59
49
|
});
|
|
60
50
|
});
|
|
61
51
|
describe("isPkgImport", () => {
|
|
@@ -70,6 +70,12 @@ export type CallbackMap = {
|
|
|
70
70
|
error: any;
|
|
71
71
|
};
|
|
72
72
|
onTrace: TraceEvent;
|
|
73
|
+
onOAuthRequired: {
|
|
74
|
+
serverName: string;
|
|
75
|
+
authUrl: string;
|
|
76
|
+
complete: Promise<void>;
|
|
77
|
+
cancel: () => void;
|
|
78
|
+
};
|
|
73
79
|
};
|
|
74
80
|
export type CallbackReturn<K extends keyof CallbackMap> = K extends "onLLMCallStart" | "onLLMCallEnd" ? MessageJSON[] | void : void;
|
|
75
81
|
export type AgencyCallbacks = {
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export type CallbackServerOptions = {
|
|
2
|
+
port?: number;
|
|
3
|
+
timeoutMs?: number;
|
|
4
|
+
};
|
|
5
|
+
export declare class CallbackServer {
|
|
6
|
+
private server;
|
|
7
|
+
private port;
|
|
8
|
+
private _state;
|
|
9
|
+
private timeoutMs;
|
|
10
|
+
private codePromise;
|
|
11
|
+
private resolveCode;
|
|
12
|
+
private rejectCode;
|
|
13
|
+
private timeoutHandle;
|
|
14
|
+
constructor(opts?: CallbackServerOptions);
|
|
15
|
+
get state(): string;
|
|
16
|
+
get callbackUrl(): string;
|
|
17
|
+
start(): Promise<string>;
|
|
18
|
+
waitForCode(): Promise<string>;
|
|
19
|
+
stop(): Promise<void>;
|
|
20
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import http from "http";
|
|
2
|
+
import { randomBytes } from "crypto";
|
|
3
|
+
const SUCCESS_HTML = `<!DOCTYPE html><html><body><h1>Authorization successful</h1><p>You can close this tab and return to your terminal.</p></body></html>`;
|
|
4
|
+
const ERROR_HTML = `<!DOCTYPE html><html><body><h1>Authorization failed</h1><p>Please try again.</p></body></html>`;
|
|
5
|
+
const DEFAULT_PORT = 19876;
|
|
6
|
+
export class CallbackServer {
|
|
7
|
+
server = null;
|
|
8
|
+
port;
|
|
9
|
+
_state;
|
|
10
|
+
timeoutMs;
|
|
11
|
+
codePromise;
|
|
12
|
+
resolveCode;
|
|
13
|
+
rejectCode;
|
|
14
|
+
timeoutHandle = null;
|
|
15
|
+
constructor(opts = {}) {
|
|
16
|
+
this.port = opts.port ?? DEFAULT_PORT;
|
|
17
|
+
this.timeoutMs = opts.timeoutMs ?? 300_000;
|
|
18
|
+
this._state = randomBytes(32).toString("hex");
|
|
19
|
+
this.codePromise = new Promise((resolve, reject) => {
|
|
20
|
+
this.resolveCode = resolve;
|
|
21
|
+
this.rejectCode = reject;
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
get state() {
|
|
25
|
+
return this._state;
|
|
26
|
+
}
|
|
27
|
+
get callbackUrl() {
|
|
28
|
+
return `http://127.0.0.1:${this.port}/oauth/callback`;
|
|
29
|
+
}
|
|
30
|
+
async start() {
|
|
31
|
+
return new Promise((resolve, reject) => {
|
|
32
|
+
this.server = http.createServer((req, res) => {
|
|
33
|
+
const url = new URL(req.url || "/", `http://127.0.0.1:${this.port}`);
|
|
34
|
+
if (url.pathname !== "/oauth/callback") {
|
|
35
|
+
res.writeHead(404);
|
|
36
|
+
res.end("Not found");
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
const code = url.searchParams.get("code");
|
|
40
|
+
const state = url.searchParams.get("state");
|
|
41
|
+
if (state !== this._state) {
|
|
42
|
+
res.writeHead(403, { "Content-Type": "text/html" });
|
|
43
|
+
res.end(ERROR_HTML);
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
if (!code) {
|
|
47
|
+
res.writeHead(400, { "Content-Type": "text/html" });
|
|
48
|
+
res.end(ERROR_HTML);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
52
|
+
res.end(SUCCESS_HTML);
|
|
53
|
+
this.resolveCode(code);
|
|
54
|
+
this.stop();
|
|
55
|
+
});
|
|
56
|
+
this.server.listen(this.port, "127.0.0.1", () => {
|
|
57
|
+
// When port is 0, the OS assigns a random port — read it back.
|
|
58
|
+
const addr = this.server.address();
|
|
59
|
+
if (addr && typeof addr === "object") {
|
|
60
|
+
this.port = addr.port;
|
|
61
|
+
}
|
|
62
|
+
resolve(this.callbackUrl);
|
|
63
|
+
});
|
|
64
|
+
this.server.on("error", reject);
|
|
65
|
+
this.timeoutHandle = setTimeout(() => {
|
|
66
|
+
this.rejectCode(new Error("OAuth callback timed out"));
|
|
67
|
+
this.stop();
|
|
68
|
+
}, this.timeoutMs);
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
async waitForCode() {
|
|
72
|
+
return this.codePromise;
|
|
73
|
+
}
|
|
74
|
+
async stop() {
|
|
75
|
+
if (this.timeoutHandle) {
|
|
76
|
+
clearTimeout(this.timeoutHandle);
|
|
77
|
+
this.timeoutHandle = null;
|
|
78
|
+
}
|
|
79
|
+
if (this.server) {
|
|
80
|
+
return new Promise((resolve) => {
|
|
81
|
+
this.server.close(() => resolve());
|
|
82
|
+
this.server = null;
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { CallbackServer } from "./callbackServer.js";
|
|
3
|
+
// Use port 0 in tests so the OS assigns a random available port,
|
|
4
|
+
// avoiding collisions with other tests or the default port.
|
|
5
|
+
const testOpts = { port: 0 };
|
|
6
|
+
describe("CallbackServer", () => {
|
|
7
|
+
it("should start and return a URL with the correct port", async () => {
|
|
8
|
+
const server = new CallbackServer(testOpts);
|
|
9
|
+
const url = await server.start();
|
|
10
|
+
expect(url).toMatch(/^http:\/\/127\.0\.0\.1:\d+\/oauth\/callback$/);
|
|
11
|
+
await server.stop();
|
|
12
|
+
});
|
|
13
|
+
it("should use the default fixed port when no port is specified", () => {
|
|
14
|
+
const server = new CallbackServer();
|
|
15
|
+
// Before start(), callbackUrl reflects the configured port
|
|
16
|
+
expect(server.callbackUrl).toBe("http://127.0.0.1:19876/oauth/callback");
|
|
17
|
+
});
|
|
18
|
+
it("should resolve with the authorization code when callback is received", async () => {
|
|
19
|
+
const server = new CallbackServer(testOpts);
|
|
20
|
+
const url = await server.start();
|
|
21
|
+
const state = server.state;
|
|
22
|
+
const callbackUrl = `${url}?code=auth-code-123&state=${state}`;
|
|
23
|
+
const response = await fetch(callbackUrl);
|
|
24
|
+
expect(response.ok).toBe(true);
|
|
25
|
+
const code = await server.waitForCode();
|
|
26
|
+
expect(code).toBe("auth-code-123");
|
|
27
|
+
await server.stop();
|
|
28
|
+
});
|
|
29
|
+
it("should reject when state parameter doesn't match", async () => {
|
|
30
|
+
const server = new CallbackServer(testOpts);
|
|
31
|
+
const url = await server.start();
|
|
32
|
+
const callbackUrl = `${url}?code=auth-code-123&state=wrong-state`;
|
|
33
|
+
const response = await fetch(callbackUrl);
|
|
34
|
+
expect(response.status).toBe(403);
|
|
35
|
+
await server.stop();
|
|
36
|
+
});
|
|
37
|
+
it("should reject when code is missing", async () => {
|
|
38
|
+
const server = new CallbackServer(testOpts);
|
|
39
|
+
const url = await server.start();
|
|
40
|
+
const state = server.state;
|
|
41
|
+
const callbackUrl = `${url}?state=${state}`;
|
|
42
|
+
const response = await fetch(callbackUrl);
|
|
43
|
+
expect(response.status).toBe(400);
|
|
44
|
+
await server.stop();
|
|
45
|
+
});
|
|
46
|
+
it("should time out if no callback is received", async () => {
|
|
47
|
+
const server = new CallbackServer({ ...testOpts, timeoutMs: 200 });
|
|
48
|
+
await server.start();
|
|
49
|
+
await expect(server.waitForCode()).rejects.toThrow(/timed out/i);
|
|
50
|
+
await server.stop();
|
|
51
|
+
});
|
|
52
|
+
it("should stop cleanly even if no callback was received", async () => {
|
|
53
|
+
const server = new CallbackServer(testOpts);
|
|
54
|
+
await server.start();
|
|
55
|
+
await server.stop();
|
|
56
|
+
});
|
|
57
|
+
});
|
|
@@ -1,11 +1,20 @@
|
|
|
1
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
1
2
|
import type { McpServerConfig, McpTool } from "./types.js";
|
|
3
|
+
/** Returns a connected MCP Client. Used by OAuthConnector. */
|
|
4
|
+
export type ConnectorFn = () => Promise<{
|
|
5
|
+
client: Client;
|
|
6
|
+
}>;
|
|
7
|
+
export type McpConnectionOptions = {
|
|
8
|
+
connector?: ConnectorFn;
|
|
9
|
+
};
|
|
2
10
|
export declare class McpConnection {
|
|
3
11
|
private client;
|
|
4
12
|
private serverName;
|
|
5
13
|
private config;
|
|
6
14
|
private tools;
|
|
7
15
|
private connected;
|
|
8
|
-
|
|
16
|
+
private connector;
|
|
17
|
+
constructor(serverName: string, config: McpServerConfig, options?: McpConnectionOptions);
|
|
9
18
|
connect(): Promise<void>;
|
|
10
19
|
getTools(): McpTool[];
|
|
11
20
|
callTool(toolName: string, args: Record<string, unknown>): Promise<string>;
|
|
@@ -1,34 +1,44 @@
|
|
|
1
1
|
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
2
2
|
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
3
3
|
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
4
|
+
import { isHttpServer, isStdioServer } from "./types.js";
|
|
4
5
|
export class McpConnection {
|
|
5
6
|
client;
|
|
6
7
|
serverName;
|
|
7
8
|
config;
|
|
8
9
|
tools = [];
|
|
9
10
|
connected = false;
|
|
10
|
-
|
|
11
|
+
connector;
|
|
12
|
+
constructor(serverName, config, options = {}) {
|
|
11
13
|
this.serverName = serverName;
|
|
12
14
|
this.config = config;
|
|
15
|
+
this.connector = options.connector;
|
|
13
16
|
this.client = new Client({
|
|
14
17
|
name: "agency-lang",
|
|
15
18
|
version: "1.0.0",
|
|
16
19
|
});
|
|
17
20
|
}
|
|
18
21
|
async connect() {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
+
if (this.connector) {
|
|
23
|
+
const result = await this.connector();
|
|
24
|
+
this.client = result.client;
|
|
22
25
|
}
|
|
23
|
-
else {
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
26
|
+
else if (isHttpServer(this.config)) {
|
|
27
|
+
const opts = {};
|
|
28
|
+
if (this.config.headers) {
|
|
29
|
+
opts.requestInit = { headers: this.config.headers };
|
|
30
|
+
}
|
|
31
|
+
const transport = new StreamableHTTPClientTransport(new URL(this.config.url), opts);
|
|
32
|
+
await this.client.connect(transport);
|
|
33
|
+
}
|
|
34
|
+
else if (isStdioServer(this.config)) {
|
|
35
|
+
const transport = new StdioClientTransport({
|
|
36
|
+
command: this.config.command,
|
|
37
|
+
args: this.config.args,
|
|
38
|
+
env: this.config.env,
|
|
29
39
|
});
|
|
40
|
+
await this.client.connect(transport);
|
|
30
41
|
}
|
|
31
|
-
await this.client.connect(transport);
|
|
32
42
|
this.connected = true;
|
|
33
43
|
const result = await this.client.listTools();
|
|
34
44
|
this.tools = (result.tools || []).map((tool) => ({
|
|
@@ -33,3 +33,26 @@ describe("McpConnection", () => {
|
|
|
33
33
|
await expect(conn.connect()).rejects.toThrow();
|
|
34
34
|
});
|
|
35
35
|
});
|
|
36
|
+
describe("McpConnection with connector", () => {
|
|
37
|
+
it("should call the connector function when connecting", async () => {
|
|
38
|
+
let connectorCalled = false;
|
|
39
|
+
const conn = new McpConnection("test", {
|
|
40
|
+
type: "http",
|
|
41
|
+
url: "https://example.com/mcp",
|
|
42
|
+
auth: "oauth",
|
|
43
|
+
}, {
|
|
44
|
+
connector: async () => {
|
|
45
|
+
connectorCalled = true;
|
|
46
|
+
throw new Error("mock connector");
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
await expect(conn.connect()).rejects.toThrow("mock connector");
|
|
50
|
+
expect(connectorCalled).toBe(true);
|
|
51
|
+
});
|
|
52
|
+
it("should not use connector for stdio servers without one", async () => {
|
|
53
|
+
const conn = new McpConnection("bad", {
|
|
54
|
+
command: "nonexistent-command",
|
|
55
|
+
});
|
|
56
|
+
await expect(conn.connect()).rejects.toThrow();
|
|
57
|
+
});
|
|
58
|
+
});
|