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/README.md +303 -0
- package/dist/auth.d.ts +9 -0
- package/dist/auth.d.ts.map +1 -0
- package/dist/auth.js +272 -0
- package/dist/auth.js.map +1 -0
- package/dist/caller.d.ts +9 -0
- package/dist/caller.d.ts.map +1 -0
- package/dist/caller.js +110 -0
- package/dist/caller.js.map +1 -0
- package/dist/config.d.ts +32 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +288 -0
- package/dist/config.js.map +1 -0
- package/dist/discovery.d.ts +6 -0
- package/dist/discovery.d.ts.map +1 -0
- package/dist/discovery.js +97 -0
- package/dist/discovery.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +509 -0
- package/dist/index.js.map +1 -0
- package/dist/init.d.ts +3 -0
- package/dist/init.d.ts.map +1 -0
- package/dist/init.js +206 -0
- package/dist/init.js.map +1 -0
- package/dist/types.d.ts +143 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +7 -0
- package/dist/types.js.map +1 -0
- package/package.json +27 -0
- package/src/auth.ts +329 -0
- package/src/caller.ts +130 -0
- package/src/config.ts +340 -0
- package/src/discovery.ts +144 -0
- package/src/index.ts +610 -0
- package/src/init.ts +254 -0
- package/src/types.ts +165 -0
- package/tsconfig.json +19 -0
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
|
+
}
|