agent-gateway-mcp 0.1.0

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/src/auth.ts ADDED
@@ -0,0 +1,329 @@
1
+ import http from "http";
2
+ import { loadConfig, getToken, storeToken, storeConnection } from "./config.js";
3
+ import { fetchManifest } from "./discovery.js";
4
+ import type { StoredToken, Manifest } from "./types.js";
5
+
6
+ export interface AuthResult {
7
+ success: boolean;
8
+ message: string;
9
+ token?: StoredToken;
10
+ }
11
+
12
+ export async function authenticate(domain: string): Promise<AuthResult> {
13
+ // Check existing token
14
+ const existing = getToken(domain);
15
+ if (existing && existing.access_token) {
16
+ return {
17
+ success: true,
18
+ message: `Already connected to ${domain}.`,
19
+ token: existing,
20
+ };
21
+ }
22
+
23
+ // If token exists but expired, try refresh
24
+ if (existing && !existing.access_token && existing.refresh_token) {
25
+ const refreshed = await tryRefreshToken(domain, existing);
26
+ if (refreshed) {
27
+ return {
28
+ success: true,
29
+ message: `Token refreshed for ${domain}.`,
30
+ token: refreshed,
31
+ };
32
+ }
33
+ }
34
+
35
+ // Fetch manifest to get auth requirements
36
+ let manifest: Manifest;
37
+ try {
38
+ manifest = await fetchManifest(domain);
39
+ } catch (err) {
40
+ return {
41
+ success: false,
42
+ message: `Cannot reach ${domain}: ${err instanceof Error ? err.message : "unknown error"}`,
43
+ };
44
+ }
45
+
46
+ const auth = manifest.auth;
47
+
48
+ if (auth.type === "none") {
49
+ const token: StoredToken = {
50
+ domain,
51
+ type: "api_key",
52
+ access_token: "none",
53
+ };
54
+ storeToken(token);
55
+ storeConnection({
56
+ domain,
57
+ service_name: manifest.name,
58
+ auth_type: "none",
59
+ token,
60
+ connected_at: new Date().toISOString(),
61
+ });
62
+ return {
63
+ success: true,
64
+ message: `${manifest.name} requires no authentication. Connected.`,
65
+ token,
66
+ };
67
+ }
68
+
69
+ if (auth.type === "api_key") {
70
+ return {
71
+ success: false,
72
+ message: [
73
+ `${manifest.name} requires an API key.`,
74
+ auth.setup_url ? `Get your key at: ${auth.setup_url}` : "",
75
+ `Once you have it, the agent should call the auth tool with:`,
76
+ ` domain: "${domain}"`,
77
+ `Then provide the API key when prompted.`,
78
+ ``,
79
+ `Or set the key manually: the header is "${auth.header ?? "Authorization"}"${auth.prefix ? ` with prefix "${auth.prefix}"` : ""}.`,
80
+ ]
81
+ .filter(Boolean)
82
+ .join("\n"),
83
+ };
84
+ }
85
+
86
+ if (auth.type === "oauth2") {
87
+ return startOAuth2Flow(domain, manifest);
88
+ }
89
+
90
+ return {
91
+ success: false,
92
+ message: `Unknown auth type: ${auth.type}`,
93
+ };
94
+ }
95
+
96
+ export async function storeApiKey(
97
+ domain: string,
98
+ apiKey: string
99
+ ): Promise<AuthResult> {
100
+ let manifest: Manifest;
101
+ try {
102
+ manifest = await fetchManifest(domain);
103
+ } catch (err) {
104
+ return {
105
+ success: false,
106
+ message: `Cannot reach ${domain}: ${err instanceof Error ? err.message : "unknown error"}`,
107
+ };
108
+ }
109
+
110
+ const token: StoredToken = {
111
+ domain,
112
+ type: "api_key",
113
+ access_token: apiKey,
114
+ };
115
+ storeToken(token);
116
+ storeConnection({
117
+ domain,
118
+ service_name: manifest.name,
119
+ auth_type: "api_key",
120
+ token,
121
+ connected_at: new Date().toISOString(),
122
+ });
123
+
124
+ return {
125
+ success: true,
126
+ message: `API key stored for ${manifest.name}. Connected.`,
127
+ token,
128
+ };
129
+ }
130
+
131
+ async function tryRefreshToken(
132
+ domain: string,
133
+ existing: StoredToken
134
+ ): Promise<StoredToken | null> {
135
+ try {
136
+ const manifest = await fetchManifest(domain);
137
+ if (manifest.auth.type !== "oauth2" || !manifest.auth.token_url) {
138
+ return null;
139
+ }
140
+
141
+ const body = new URLSearchParams({
142
+ grant_type: "refresh_token",
143
+ refresh_token: existing.refresh_token!,
144
+ });
145
+
146
+ const res = await fetch(manifest.auth.token_url, {
147
+ method: "POST",
148
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
149
+ body: body.toString(),
150
+ signal: AbortSignal.timeout(10000),
151
+ });
152
+
153
+ if (!res.ok) return null;
154
+
155
+ const data = (await res.json()) as {
156
+ access_token: string;
157
+ refresh_token?: string;
158
+ expires_in?: number;
159
+ };
160
+
161
+ const token: StoredToken = {
162
+ domain,
163
+ type: "oauth2",
164
+ access_token: data.access_token,
165
+ refresh_token: data.refresh_token ?? existing.refresh_token,
166
+ expires_at: data.expires_in
167
+ ? Date.now() + data.expires_in * 1000
168
+ : undefined,
169
+ scopes: existing.scopes,
170
+ };
171
+
172
+ storeToken(token);
173
+ return token;
174
+ } catch {
175
+ return null;
176
+ }
177
+ }
178
+
179
+ async function startOAuth2Flow(
180
+ domain: string,
181
+ manifest: Manifest
182
+ ): Promise<AuthResult> {
183
+ const auth = manifest.auth;
184
+ if (!auth.authorization_url || !auth.token_url) {
185
+ return {
186
+ success: false,
187
+ message: "OAuth2 configuration missing authorization_url or token_url.",
188
+ };
189
+ }
190
+
191
+ const config = loadConfig();
192
+ const port = config.auth_callback_port;
193
+ const redirectUri = `http://localhost:${port}/callback`;
194
+ const state = Math.random().toString(36).substring(2);
195
+
196
+ const authUrl = new URL(auth.authorization_url);
197
+ authUrl.searchParams.set("response_type", "code");
198
+ authUrl.searchParams.set("redirect_uri", redirectUri);
199
+ authUrl.searchParams.set("state", state);
200
+ if (auth.scopes?.length) {
201
+ authUrl.searchParams.set("scope", auth.scopes.join(" "));
202
+ }
203
+
204
+ // Start local callback server
205
+ const tokenPromise = new Promise<StoredToken>((resolve, reject) => {
206
+ const timeout = setTimeout(() => {
207
+ server.close();
208
+ reject(new Error("OAuth timeout — no callback received within 120 seconds."));
209
+ }, 120000);
210
+
211
+ const server = http.createServer(async (req, res) => {
212
+ const url = new URL(req.url ?? "/", `http://localhost:${port}`);
213
+
214
+ if (url.pathname !== "/callback") {
215
+ res.writeHead(404);
216
+ res.end("Not found");
217
+ return;
218
+ }
219
+
220
+ const code = url.searchParams.get("code");
221
+ const returnedState = url.searchParams.get("state");
222
+ const error = url.searchParams.get("error");
223
+
224
+ if (error) {
225
+ res.writeHead(200, { "Content-Type": "text/html" });
226
+ res.end(`<html><body><h2>Authorization failed</h2><p>${error}</p><p>You can close this tab.</p></body></html>`);
227
+ clearTimeout(timeout);
228
+ server.close();
229
+ reject(new Error(`OAuth error: ${error}`));
230
+ return;
231
+ }
232
+
233
+ if (!code || returnedState !== state) {
234
+ res.writeHead(400, { "Content-Type": "text/html" });
235
+ res.end("<html><body><h2>Invalid callback</h2><p>Missing code or state mismatch.</p></body></html>");
236
+ return;
237
+ }
238
+
239
+ // Exchange code for token
240
+ try {
241
+ const tokenBody = new URLSearchParams({
242
+ grant_type: "authorization_code",
243
+ code,
244
+ redirect_uri: redirectUri,
245
+ });
246
+
247
+ const tokenRes = await fetch(auth.token_url!, {
248
+ method: "POST",
249
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
250
+ body: tokenBody.toString(),
251
+ signal: AbortSignal.timeout(10000),
252
+ });
253
+
254
+ if (!tokenRes.ok) {
255
+ throw new Error(`Token exchange failed: HTTP ${tokenRes.status}`);
256
+ }
257
+
258
+ const tokenData = (await tokenRes.json()) as {
259
+ access_token: string;
260
+ refresh_token?: string;
261
+ expires_in?: number;
262
+ scope?: string;
263
+ };
264
+
265
+ const token: StoredToken = {
266
+ domain,
267
+ type: "oauth2",
268
+ access_token: tokenData.access_token,
269
+ refresh_token: tokenData.refresh_token,
270
+ expires_at: tokenData.expires_in
271
+ ? Date.now() + tokenData.expires_in * 1000
272
+ : undefined,
273
+ scopes: tokenData.scope?.split(" ") ?? auth.scopes,
274
+ };
275
+
276
+ res.writeHead(200, { "Content-Type": "text/html" });
277
+ res.end(
278
+ `<html><body><h2>Connected to ${manifest.name}!</h2><p>You can close this tab and return to your agent.</p></body></html>`
279
+ );
280
+
281
+ clearTimeout(timeout);
282
+ server.close();
283
+ resolve(token);
284
+ } catch (err) {
285
+ res.writeHead(500, { "Content-Type": "text/html" });
286
+ res.end(`<html><body><h2>Token exchange failed</h2><p>${err instanceof Error ? err.message : "Unknown error"}</p></body></html>`);
287
+ clearTimeout(timeout);
288
+ server.close();
289
+ reject(err);
290
+ }
291
+ });
292
+
293
+ server.listen(port, () => {
294
+ // Server ready
295
+ });
296
+ });
297
+
298
+ // Open browser
299
+ try {
300
+ const open = (await import("open")).default;
301
+ await open(authUrl.toString());
302
+ } catch {
303
+ // If open fails, just return the URL
304
+ }
305
+
306
+ const openMessage = `Opening browser for authorization...\n\nIf the browser didn't open, visit:\n${authUrl.toString()}\n\nWaiting for callback on http://localhost:${port}/callback...`;
307
+
308
+ try {
309
+ const token = await tokenPromise;
310
+ storeToken(token);
311
+ storeConnection({
312
+ domain,
313
+ service_name: manifest.name,
314
+ auth_type: "oauth2",
315
+ token,
316
+ connected_at: new Date().toISOString(),
317
+ });
318
+ return {
319
+ success: true,
320
+ message: `${openMessage}\n\nConnected to ${manifest.name} via OAuth2.`,
321
+ token,
322
+ };
323
+ } catch (err) {
324
+ return {
325
+ success: false,
326
+ message: `${openMessage}\n\nAuth failed: ${err instanceof Error ? err.message : "unknown error"}`,
327
+ };
328
+ }
329
+ }
package/src/caller.ts ADDED
@@ -0,0 +1,130 @@
1
+ import { fetchManifest, fetchCapabilityDetail } from "./discovery.js";
2
+ import { authenticate, storeApiKey } from "./auth.js";
3
+ import { getToken } from "./config.js";
4
+ import type { CapabilityDetail, Manifest } from "./types.js";
5
+
6
+ export interface CallResult {
7
+ success: boolean;
8
+ status?: number;
9
+ data?: unknown;
10
+ error?: string;
11
+ auth_required?: boolean;
12
+ }
13
+
14
+ export async function callCapability(
15
+ domain: string,
16
+ capabilityName: string,
17
+ params: Record<string, unknown>,
18
+ apiKey?: string
19
+ ): Promise<CallResult> {
20
+ // Step 1: Ensure we have a token
21
+ let token = getToken(domain);
22
+ if (!token || !token.access_token) {
23
+ // If caller provided an API key, store it
24
+ if (apiKey) {
25
+ const authResult = await storeApiKey(domain, apiKey);
26
+ if (!authResult.success) {
27
+ return { success: false, error: authResult.message, auth_required: true };
28
+ }
29
+ token = authResult.token;
30
+ } else {
31
+ // Try to authenticate
32
+ const authResult = await authenticate(domain);
33
+ if (!authResult.success) {
34
+ return { success: false, error: authResult.message, auth_required: true };
35
+ }
36
+ token = authResult.token;
37
+ }
38
+ }
39
+
40
+ if (!token) {
41
+ return { success: false, error: "No authentication token available.", auth_required: true };
42
+ }
43
+
44
+ // Step 2: Fetch capability detail
45
+ let detail: CapabilityDetail;
46
+ let manifest: Manifest;
47
+ try {
48
+ manifest = await fetchManifest(domain);
49
+ detail = await fetchCapabilityDetail(domain, capabilityName);
50
+ } catch (err) {
51
+ return {
52
+ success: false,
53
+ error: `Failed to fetch capability detail: ${err instanceof Error ? err.message : "unknown"}`,
54
+ };
55
+ }
56
+
57
+ // Step 3: Build the request
58
+ const url = detail.endpoint.startsWith("http")
59
+ ? detail.endpoint
60
+ : `${manifest.base_url}${detail.endpoint}`;
61
+
62
+ const headers: Record<string, string> = {
63
+ "Content-Type": "application/json",
64
+ Accept: "application/json",
65
+ };
66
+
67
+ // Inject auth
68
+ if (token.access_token !== "none") {
69
+ if (token.type === "api_key") {
70
+ const authHeader = manifest.auth.header ?? "Authorization";
71
+ const prefix = manifest.auth.prefix ?? "Bearer";
72
+ headers[authHeader] = `${prefix} ${token.access_token}`;
73
+ } else {
74
+ headers["Authorization"] = `Bearer ${token.access_token}`;
75
+ }
76
+ }
77
+
78
+ // Step 4: Make the call
79
+ const method = detail.method.toUpperCase();
80
+ const fetchOpts: RequestInit = {
81
+ method,
82
+ headers,
83
+ signal: AbortSignal.timeout(30000),
84
+ };
85
+
86
+ if (method !== "GET" && method !== "HEAD") {
87
+ fetchOpts.body = JSON.stringify(params);
88
+ }
89
+
90
+ // For GET, append params as query string
91
+ let finalUrl = url;
92
+ if (method === "GET" && Object.keys(params).length > 0) {
93
+ const qs = new URLSearchParams();
94
+ for (const [key, value] of Object.entries(params)) {
95
+ qs.set(key, String(value));
96
+ }
97
+ finalUrl = `${url}?${qs.toString()}`;
98
+ }
99
+
100
+ try {
101
+ const res = await fetch(finalUrl, fetchOpts);
102
+ let body: unknown;
103
+ const contentType = res.headers.get("content-type") ?? "";
104
+ if (contentType.includes("application/json")) {
105
+ body = await res.json();
106
+ } else {
107
+ body = await res.text();
108
+ }
109
+
110
+ if (!res.ok) {
111
+ return {
112
+ success: false,
113
+ status: res.status,
114
+ data: body,
115
+ error: `Service returned HTTP ${res.status}`,
116
+ };
117
+ }
118
+
119
+ return {
120
+ success: true,
121
+ status: res.status,
122
+ data: body,
123
+ };
124
+ } catch (err) {
125
+ return {
126
+ success: false,
127
+ error: `Request failed: ${err instanceof Error ? err.message : "unknown"}`,
128
+ };
129
+ }
130
+ }