within-sdk 1.0.0 → 1.0.1
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/LICENSE +1 -1
- package/README.md +211 -77
- package/dist/thirdparty/ksuid/base-convert-int-array.js +49 -49
- package/dist/thirdparty/ksuid/base62.js +24 -24
- package/dist/thirdparty/ksuid/index.d.ts +30 -30
- package/dist/thirdparty/ksuid/index.js +201 -201
- package/package.json +43 -43
- package/dist/middleware.d.ts +0 -40
- package/dist/middleware.js +0 -562
- package/dist/network.d.ts +0 -83
- package/dist/network.js +0 -411
- package/dist/proxy.d.ts +0 -31
- package/dist/proxy.js +0 -158
- package/dist/validate.d.ts +0 -53
- package/dist/validate.js +0 -139
package/dist/network.js
DELETED
|
@@ -1,411 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* With.in Network Discovery + Cross-Vendor Tool Use
|
|
3
|
-
*
|
|
4
|
-
* Injects two tools into every SDK-equipped vendor MCP server:
|
|
5
|
-
*
|
|
6
|
-
* • find_vendor_for_task — semantic search over the With.in vendor catalog
|
|
7
|
-
* • within__use — silently provision + proxy a tool call to another vendor
|
|
8
|
-
*
|
|
9
|
-
* Both tools read the caller's identity from AsyncLocalStorage. The vendor
|
|
10
|
-
* wraps their MCP request handling in `within.runWithAuth(auth, fn)` so the
|
|
11
|
-
* tools see the correct access token + claims. See README for integration.
|
|
12
|
-
*
|
|
13
|
-
* Security posture:
|
|
14
|
-
* • The access token the vendor already validated is reused as the bearer for
|
|
15
|
-
* /api/network/*. It carries the same within_network_id / within_email /
|
|
16
|
-
* within_org_domain as the UIT would.
|
|
17
|
-
* • Fresh per-target access tokens are minted server-side and cached only
|
|
18
|
-
* for the remainder of their TTL.
|
|
19
|
-
* • within__use requires user_confirmed=true. This is a guardrail so Claude
|
|
20
|
-
* can't silently fan out — it must obtain user consent for the target
|
|
21
|
-
* vendor between find_vendor_for_task and within__use.
|
|
22
|
-
*/
|
|
23
|
-
// Authoritative list of SDK-injected network tool names. Used by the quota
|
|
24
|
-
// middleware (to exempt them from per-token counters) and by the tool-list
|
|
25
|
-
// reporter (to avoid recursively advertising network tools). Update this when
|
|
26
|
-
// adding/renaming any network tool — the prefix-based check used to do this
|
|
27
|
-
// implicitly, but find_vendor_for_task no longer carries the within__ prefix.
|
|
28
|
-
export const NETWORK_TOOL_NAMES = new Set([
|
|
29
|
-
'find_vendor_for_task',
|
|
30
|
-
'within__use',
|
|
31
|
-
]);
|
|
32
|
-
import { callRemoteMcpTool } from './proxy.js';
|
|
33
|
-
// The SDK has no @types/node dep; TS can't resolve the builtin by name, so we
|
|
34
|
-
// launder the specifier through a const-narrowed string.
|
|
35
|
-
const ASYNC_HOOKS_MOD = 'node:async_hooks';
|
|
36
|
-
const { AsyncLocalStorage } = (await import(ASYNC_HOOKS_MOD));
|
|
37
|
-
const _storage = new AsyncLocalStorage();
|
|
38
|
-
// Accessor so the host can read the current auth inside tool handlers.
|
|
39
|
-
export const authContextStorage = {
|
|
40
|
-
getStore() {
|
|
41
|
-
return _storage.getStore();
|
|
42
|
-
},
|
|
43
|
-
};
|
|
44
|
-
export function runWithAuth(auth, fn) {
|
|
45
|
-
return _storage.run(auth, fn);
|
|
46
|
-
}
|
|
47
|
-
/**
|
|
48
|
-
* Returns the definitions of the two network tools. Register them on your
|
|
49
|
-
* vendor's MCP server exactly once at startup.
|
|
50
|
-
*
|
|
51
|
-
* Intentionally not tied to a specific MCP server package — the vendor calls
|
|
52
|
-
* `server.registerTool(def.name, def.config, def.handler)` with its own
|
|
53
|
-
* zod-compatible schema wrapper. See `registerNetworkTools` below for a
|
|
54
|
-
* helper that does this for the @modelcontextprotocol/server package.
|
|
55
|
-
*/
|
|
56
|
-
export function buildNetworkTools(config, z) {
|
|
57
|
-
const tokenCache = new Map();
|
|
58
|
-
async function getProvisionedToken(identityKey, targetSlug, auth) {
|
|
59
|
-
const cacheKey = `${identityKey}:${targetSlug}`;
|
|
60
|
-
const cached = tokenCache.get(cacheKey);
|
|
61
|
-
if (cached && cached.expiresAtMs - Date.now() > 60_000) {
|
|
62
|
-
return cached;
|
|
63
|
-
}
|
|
64
|
-
const res = await fetch(`${config.authServerUrl}/api/network/provision`, {
|
|
65
|
-
method: 'POST',
|
|
66
|
-
headers: {
|
|
67
|
-
'Content-Type': 'application/json',
|
|
68
|
-
Authorization: `Bearer ${auth.token}`,
|
|
69
|
-
},
|
|
70
|
-
body: JSON.stringify({
|
|
71
|
-
target_vendor_slug: targetSlug,
|
|
72
|
-
calling_vendor_slug: config.vendorSlug,
|
|
73
|
-
}),
|
|
74
|
-
});
|
|
75
|
-
if (!res.ok) {
|
|
76
|
-
const body = await res.text();
|
|
77
|
-
throw new ProvisionError(res.status, body);
|
|
78
|
-
}
|
|
79
|
-
const data = (await res.json());
|
|
80
|
-
if (!data.mcp_server_url) {
|
|
81
|
-
throw new Error(`Vendor ${targetSlug} has no registered mcp_server_url`);
|
|
82
|
-
}
|
|
83
|
-
const entry = {
|
|
84
|
-
accessToken: data.access_token,
|
|
85
|
-
mcpServerUrl: data.mcp_server_url,
|
|
86
|
-
expiresAtMs: Date.now() + data.expires_in * 1000,
|
|
87
|
-
trial: data.trial,
|
|
88
|
-
};
|
|
89
|
-
tokenCache.set(cacheKey, entry);
|
|
90
|
-
return entry;
|
|
91
|
-
}
|
|
92
|
-
return {
|
|
93
|
-
discover: {
|
|
94
|
-
name: 'find_vendor_for_task',
|
|
95
|
-
config: {
|
|
96
|
-
title: 'Find a vendor that can perform a task',
|
|
97
|
-
description: "Discover tools and capabilities from third-party vendors in the With.in network. Use for tool discovery, vendor lookup, or capability search whenever the user's request might be served by an external vendor capability not already covered by this server's tools — e.g. specialized data lookups, industry-specific actions, niche integrations, or any 'find a tool that can do X' situation. Returns a ranked list of candidate vendors with their tool names; pass the chosen vendor_slug + tool_name to within__use to invoke. Search by intent in natural language (e.g. 'add a contact to a CRM', 'lookup company revenue', 'send a fax'); the network handles vendor matching.",
|
|
98
|
-
inputSchema: z.object({
|
|
99
|
-
intent: z.string().describe('Natural-language description of the capability you need. Examples: "add a contact to a CRM", "log a sales activity", "lookup company revenue or industry data", "find a calendar tool to schedule a meeting", "send an email or fax", "find a tool that can do X". Cover CRM/contacts, sales pipeline, lead management, data lookup, calendaring, communication, finance, accounting, productivity, and any other third-party capability.'),
|
|
100
|
-
exclude_vendor_slugs: z
|
|
101
|
-
.array(z.string())
|
|
102
|
-
.optional()
|
|
103
|
-
.describe('Vendor slugs to exclude from results (e.g. already tried).'),
|
|
104
|
-
}),
|
|
105
|
-
// Pin into Claude's tool list regardless of Tool Search defaults. Without
|
|
106
|
-
// this, Claude Code v2.1.121+ and the Sonnet/Opus 4+ API tier defer all
|
|
107
|
-
// MCP tools — find_vendor_for_task is invisible until Claude searches
|
|
108
|
-
// for it by keyword, which it usually doesn't think to do for a network
|
|
109
|
-
// discovery tool. Clients that don't recognize this annotation ignore
|
|
110
|
-
// it and fall back to default deferral, which is the worst case = the
|
|
111
|
-
// pre-fix status quo.
|
|
112
|
-
_meta: { 'anthropic/alwaysLoad': true },
|
|
113
|
-
},
|
|
114
|
-
handler: async ({ intent, exclude_vendor_slugs }) => {
|
|
115
|
-
const auth = authContextStorage.getStore();
|
|
116
|
-
if (!auth?.token) {
|
|
117
|
-
return errorResult('find_vendor_for_task requires a With.in-authenticated request');
|
|
118
|
-
}
|
|
119
|
-
try {
|
|
120
|
-
const res = await fetch(`${config.authServerUrl}/api/network/discover`, {
|
|
121
|
-
method: 'POST',
|
|
122
|
-
headers: {
|
|
123
|
-
'Content-Type': 'application/json',
|
|
124
|
-
Authorization: `Bearer ${auth.token}`,
|
|
125
|
-
},
|
|
126
|
-
body: JSON.stringify({ intent, exclude_vendor_slugs }),
|
|
127
|
-
});
|
|
128
|
-
if (!res.ok) {
|
|
129
|
-
const body = await res.text();
|
|
130
|
-
return errorResult(`Network discovery failed (${res.status}): ${body}`);
|
|
131
|
-
}
|
|
132
|
-
const data = (await res.json());
|
|
133
|
-
if (data.results.length === 0) {
|
|
134
|
-
return textResult(`No With.in vendors matched "${intent}". The network may not yet cover this capability. Continue with only the tools you already have.`);
|
|
135
|
-
}
|
|
136
|
-
const lines = [
|
|
137
|
-
`Found ${data.results.length} vendor${data.results.length === 1 ? '' : 's'} matching "${intent}":`,
|
|
138
|
-
'',
|
|
139
|
-
];
|
|
140
|
-
for (const r of data.results) {
|
|
141
|
-
lines.push(`• ${r.name} (${r.vendor_slug})${r.category ? ` — ${r.category}` : ''}`);
|
|
142
|
-
if (r.one_line)
|
|
143
|
-
lines.push(` ${r.one_line}`);
|
|
144
|
-
if (r.relevant_tools.length > 0) {
|
|
145
|
-
const toolList = r.relevant_tools.map((t) => t.name).join(', ');
|
|
146
|
-
lines.push(` Tools: ${toolList}`);
|
|
147
|
-
}
|
|
148
|
-
if (r.upgrade_url)
|
|
149
|
-
lines.push(` Subscribe: ${r.upgrade_url}`);
|
|
150
|
-
if (r.pricing_url)
|
|
151
|
-
lines.push(` Pricing: ${r.pricing_url}`);
|
|
152
|
-
lines.push('');
|
|
153
|
-
}
|
|
154
|
-
lines.push('Ask the user which vendor to use, then call within__use with the chosen vendor_slug and target tool.');
|
|
155
|
-
lines.push('If a vendor\'s trial is exhausted, share its Subscribe link with the user so they can upgrade.');
|
|
156
|
-
return textResult(lines.join('\n'));
|
|
157
|
-
}
|
|
158
|
-
catch (err) {
|
|
159
|
-
return errorResult(`find_vendor_for_task error: ${err.message ?? String(err)}`);
|
|
160
|
-
}
|
|
161
|
-
},
|
|
162
|
-
},
|
|
163
|
-
use: {
|
|
164
|
-
name: 'within__use',
|
|
165
|
-
config: {
|
|
166
|
-
title: 'Use a tool from another With.in vendor',
|
|
167
|
-
description: 'Invoke a tool on another With.in vendor that was selected via find_vendor_for_task. Provisions a trial automatically — no extra login. Requires user_confirmed=true as a guardrail: first ask the user to approve the target vendor, then call this tool with user_confirmed=true.',
|
|
168
|
-
inputSchema: z.object({
|
|
169
|
-
target_vendor_slug: z.string().describe('The vendor_slug returned by find_vendor_for_task.'),
|
|
170
|
-
tool_name: z.string().describe('The tool to invoke on the target vendor.'),
|
|
171
|
-
// Zod v4 requires explicit key + value schemas for z.record. Using
|
|
172
|
-
// the single-arg form produces a schema with no `_zod` internal and
|
|
173
|
-
// causes MCP's JSON-schema converter to throw
|
|
174
|
-
// "Cannot read properties of undefined (reading '_zod')" at
|
|
175
|
-
// tools/list time.
|
|
176
|
-
tool_arguments: z
|
|
177
|
-
.record(z.string(), z.unknown())
|
|
178
|
-
.describe('Arguments object to pass to the remote tool. Must match the remote tool schema.'),
|
|
179
|
-
user_confirmed: z.boolean().describe('Set to true ONLY after the user has explicitly approved this vendor.'),
|
|
180
|
-
}),
|
|
181
|
-
},
|
|
182
|
-
handler: async ({ target_vendor_slug, tool_name, tool_arguments, user_confirmed, }) => {
|
|
183
|
-
const auth = authContextStorage.getStore();
|
|
184
|
-
if (!auth?.token || !auth.withinClaims) {
|
|
185
|
-
return errorResult('within__use requires a With.in-authenticated request');
|
|
186
|
-
}
|
|
187
|
-
if (!user_confirmed) {
|
|
188
|
-
return errorResult('user_confirmed must be true. Ask the user to approve the target vendor before calling within__use.');
|
|
189
|
-
}
|
|
190
|
-
const identityKey = auth.withinClaims.within_network_id ?? auth.withinClaims.sub;
|
|
191
|
-
let token;
|
|
192
|
-
try {
|
|
193
|
-
token = await getProvisionedToken(identityKey, target_vendor_slug, auth);
|
|
194
|
-
}
|
|
195
|
-
catch (err) {
|
|
196
|
-
if (err instanceof ProvisionError) {
|
|
197
|
-
return errorResult(`Could not provision access to ${target_vendor_slug}: ${err.message}`);
|
|
198
|
-
}
|
|
199
|
-
return errorResult(`Provisioning error: ${err.message ?? String(err)}`);
|
|
200
|
-
}
|
|
201
|
-
const continueHint = token.trial.handoff_email
|
|
202
|
-
? ` To continue, please email ${token.trial.handoff_email}.`
|
|
203
|
-
: token.trial.upgrade_url
|
|
204
|
-
? ` Subscribe to continue: ${token.trial.upgrade_url}`
|
|
205
|
-
: '';
|
|
206
|
-
if (token.trial.calls_remaining === 0) {
|
|
207
|
-
return errorResult(`${target_vendor_slug} trial is exhausted — no calls remaining.${continueHint}`);
|
|
208
|
-
}
|
|
209
|
-
try {
|
|
210
|
-
const result = await callRemoteMcpTool(token.mcpServerUrl, token.accessToken, tool_name, tool_arguments);
|
|
211
|
-
// The remote server returned a JSON-RPC envelope. Hand its `result`
|
|
212
|
-
// (or `error`) through to Claude as-is so tool-call semantics survive
|
|
213
|
-
// the proxy hop.
|
|
214
|
-
if (typeof result.body === 'object' && result.body !== null) {
|
|
215
|
-
const envelope = result.body;
|
|
216
|
-
if (envelope.result?.content) {
|
|
217
|
-
return envelope.result;
|
|
218
|
-
}
|
|
219
|
-
if (envelope.error) {
|
|
220
|
-
return errorResult(`Remote tool error: ${envelope.error.message ?? 'unknown'}.${continueHint}`);
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
return errorResult(`Unexpected response from ${target_vendor_slug}: ${JSON.stringify(result.body).slice(0, 300)}`);
|
|
224
|
-
}
|
|
225
|
-
catch (err) {
|
|
226
|
-
return errorResult(`Could not reach ${target_vendor_slug}: ${err.message ?? String(err)}.${continueHint}`);
|
|
227
|
-
}
|
|
228
|
-
},
|
|
229
|
-
},
|
|
230
|
-
};
|
|
231
|
-
}
|
|
232
|
-
/**
|
|
233
|
-
* Convenience helper for @modelcontextprotocol/server-style MCP servers.
|
|
234
|
-
* Pass your McpServer and zod module and the tools are registered in one call.
|
|
235
|
-
*
|
|
236
|
-
* Also fire-and-forgets a POST /oauth/report-tools so With.in's vendor record
|
|
237
|
-
* gets populated with this server's full tool list (excluding the SDK's own
|
|
238
|
-
* network tools — see NETWORK_TOOL_NAMES). That's what `find_vendor_for_task`
|
|
239
|
-
* returns as `relevant_tools` and what the embedding worker folds into the
|
|
240
|
-
* ranking input. Deduped by hash so repeat sessions don't spam the auth server.
|
|
241
|
-
*/
|
|
242
|
-
// Server-level instructions surfaced via the MCP `initialize` response. With
|
|
243
|
-
// Tool Search enabled (default in Claude Code v2.1.121+ and the Sonnet/Opus
|
|
244
|
-
// 4+ API tier), instructions help Claude understand when to *search* for the
|
|
245
|
-
// network tools — they sit in a different channel than tool descriptions and
|
|
246
|
-
// some clients treat them as more trusted vendor context. Truncated past 2KB
|
|
247
|
-
// by Claude Code, so keep this tight.
|
|
248
|
-
const WITHIN_SERVER_INSTRUCTIONS = [
|
|
249
|
-
"This MCP server participates in the With.in vendor network — a meta-tool layer giving you access to tools from many third-party vendors via a discovery + invocation pattern.",
|
|
250
|
-
"When the user asks for something that needs an external service or capability not already exposed as one of this server's tools (specialized lookups, niche actions, integrations, third-party APIs), call find_vendor_for_task with a natural-language intent. The result lists candidate vendors with their tool names; pass the chosen vendor_slug + tool_name to within__use along with the vendor-specific arguments.",
|
|
251
|
-
"Prefer this discovery path over telling the user 'I don't have a tool for that' — the network exists precisely to fill those gaps.",
|
|
252
|
-
].join(' ');
|
|
253
|
-
// Module-level cache of vendor network status. Read on every new MCP session
|
|
254
|
-
// to decide whether to register find_vendor_for_task / within__use. TTL is
|
|
255
|
-
// short so dashboard toggles propagate near-immediately while still smoothing
|
|
256
|
-
// rapid burst sessions (one fetch per ~5s, not per session). `undefined`
|
|
257
|
-
// means we haven't fetched yet; on fetch failure we default to
|
|
258
|
-
// discoverable=true so a transient auth-server outage doesn't silently strip
|
|
259
|
-
// the network tools from production vendors.
|
|
260
|
-
const NETWORK_STATUS_TTL_MS = 5_000;
|
|
261
|
-
const networkStatusCache = new Map();
|
|
262
|
-
/** Diagnostic — exposes current cached network-status (or undefined if never fetched). */
|
|
263
|
-
export function getNetworkStatusCache(slug) {
|
|
264
|
-
return networkStatusCache.get(slug);
|
|
265
|
-
}
|
|
266
|
-
async function fetchNetworkStatus(config) {
|
|
267
|
-
const cached = networkStatusCache.get(config.vendorSlug);
|
|
268
|
-
if (cached && Date.now() - cached.fetchedAtMs < NETWORK_STATUS_TTL_MS) {
|
|
269
|
-
console.log(`[within:network-status] cache hit slug=${config.vendorSlug} discoverable=${cached.discoverable}`);
|
|
270
|
-
return cached.discoverable;
|
|
271
|
-
}
|
|
272
|
-
const url = `${config.authServerUrl}/api/vendors/${encodeURIComponent(config.vendorSlug)}/network-status`;
|
|
273
|
-
try {
|
|
274
|
-
const res = await fetch(url);
|
|
275
|
-
if (!res.ok) {
|
|
276
|
-
// Unknown vendor / auth-server error: fail open (keep tools registered).
|
|
277
|
-
console.log(`[within:network-status] FAIL-OPEN slug=${config.vendorSlug} status=${res.status} url=${url}`);
|
|
278
|
-
networkStatusCache.set(config.vendorSlug, { discoverable: true, fetchedAtMs: Date.now() });
|
|
279
|
-
return true;
|
|
280
|
-
}
|
|
281
|
-
const data = (await res.json());
|
|
282
|
-
const discoverable = data.discoverable !== false;
|
|
283
|
-
console.log(`[within:network-status] fetched slug=${config.vendorSlug} discoverable=${discoverable} raw=${JSON.stringify(data)}`);
|
|
284
|
-
networkStatusCache.set(config.vendorSlug, { discoverable, fetchedAtMs: Date.now() });
|
|
285
|
-
return discoverable;
|
|
286
|
-
}
|
|
287
|
-
catch (err) {
|
|
288
|
-
console.log(`[within:network-status] FAIL-OPEN slug=${config.vendorSlug} error=${err?.message ?? err} url=${url}`);
|
|
289
|
-
networkStatusCache.set(config.vendorSlug, { discoverable: true, fetchedAtMs: Date.now() });
|
|
290
|
-
return true;
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
export async function registerNetworkTools(mcpServer, config, z) {
|
|
294
|
-
const discoverable = await fetchNetworkStatus(config);
|
|
295
|
-
if (!discoverable) {
|
|
296
|
-
// Vendor opted out of the network. Don't register meta-tools, don't
|
|
297
|
-
// mutate server instructions, don't report tools. The vendor's own tools
|
|
298
|
-
// still work normally — only With.in network discovery is suppressed.
|
|
299
|
-
return;
|
|
300
|
-
}
|
|
301
|
-
const tools = buildNetworkTools(config, z);
|
|
302
|
-
mcpServer.registerTool(tools.discover.name, tools.discover.config, tools.discover.handler);
|
|
303
|
-
mcpServer.registerTool(tools.use.name, tools.use.config, tools.use.handler);
|
|
304
|
-
// Inject server instructions into the MCP `initialize` response. McpServer's
|
|
305
|
-
// public surface only accepts instructions via the constructor's options
|
|
306
|
-
// object, but the vendor constructs the McpServer in their own code — we
|
|
307
|
-
// can't make them know about this. Mutating `server._instructions` before
|
|
308
|
-
// connect() is read at initialize time (see @modelcontextprotocol/server's
|
|
309
|
-
// Server._oninitialize: instructions are pulled from the field on each
|
|
310
|
-
// initialize call). The leading-underscore field is not part of the
|
|
311
|
-
// formally-public API, so we guard with try/catch — if a future SDK rev
|
|
312
|
-
// seals it, we fall back to the no-instructions baseline rather than
|
|
313
|
-
// breaking the session.
|
|
314
|
-
try {
|
|
315
|
-
const underlying = mcpServer?.server;
|
|
316
|
-
if (underlying && typeof underlying === 'object') {
|
|
317
|
-
underlying._instructions = WITHIN_SERVER_INSTRUCTIONS;
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
catch {
|
|
321
|
-
// Non-fatal: instructions are an enhancement, not a requirement.
|
|
322
|
-
}
|
|
323
|
-
// Fire-and-forget the tool-list report. Runs async so it never blocks
|
|
324
|
-
// session init. Errors are logged, not thrown.
|
|
325
|
-
reportVendorToolsFromMcpServer(mcpServer, config).catch((err) => {
|
|
326
|
-
console.error(`[within] tool-list report failed: ${err?.message ?? err}`);
|
|
327
|
-
});
|
|
328
|
-
}
|
|
329
|
-
// Module-level dedupe: once a given slug has been reported with a given tool
|
|
330
|
-
// hash, we skip subsequent calls. The vendor's index-http typically rebuilds
|
|
331
|
-
// the MCP server per new session; without this, we'd POST on every session.
|
|
332
|
-
const reportedHashBySlug = new Map();
|
|
333
|
-
async function reportVendorToolsFromMcpServer(mcpServer, config) {
|
|
334
|
-
const extracted = introspectTools(mcpServer);
|
|
335
|
-
if (extracted.length === 0)
|
|
336
|
-
return;
|
|
337
|
-
const hash = simpleHash(extracted.map((t) => `${t.name}|${t.description ?? ''}`).sort().join('\n'));
|
|
338
|
-
if (reportedHashBySlug.get(config.vendorSlug) === hash)
|
|
339
|
-
return;
|
|
340
|
-
try {
|
|
341
|
-
const res = await fetch(`${config.authServerUrl}/oauth/report-tools`, {
|
|
342
|
-
method: 'POST',
|
|
343
|
-
headers: { 'Content-Type': 'application/json' },
|
|
344
|
-
body: JSON.stringify({ vendor_slug: config.vendorSlug, tools: extracted }),
|
|
345
|
-
});
|
|
346
|
-
if (res.ok) {
|
|
347
|
-
reportedHashBySlug.set(config.vendorSlug, hash);
|
|
348
|
-
}
|
|
349
|
-
}
|
|
350
|
-
catch {
|
|
351
|
-
// Network blip is OK — next session will retry.
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
|
-
/**
|
|
355
|
-
* Pull {name, description} for every tool on the MCP server except the SDK's
|
|
356
|
-
* own network tools (see NETWORK_TOOL_NAMES).
|
|
357
|
-
*
|
|
358
|
-
* Relies on `_registeredTools` being a plain object on the McpServer. It's a
|
|
359
|
-
* leading-underscore field (not part of the formal public API) so we defend
|
|
360
|
-
* in depth: if the shape changes, we return [] and the discovery system
|
|
361
|
-
* degrades gracefully — find_vendor_for_task just returns no relevant_tools,
|
|
362
|
-
* and the embedding still uses name + description + tags + capability_tags.
|
|
363
|
-
*/
|
|
364
|
-
function introspectTools(mcpServer) {
|
|
365
|
-
try {
|
|
366
|
-
const registry = mcpServer?._registeredTools;
|
|
367
|
-
if (!registry || typeof registry !== 'object')
|
|
368
|
-
return [];
|
|
369
|
-
const out = [];
|
|
370
|
-
for (const [name, tool] of Object.entries(registry)) {
|
|
371
|
-
if (typeof name !== 'string')
|
|
372
|
-
continue;
|
|
373
|
-
if (NETWORK_TOOL_NAMES.has(name))
|
|
374
|
-
continue; // don't recursively advertise network tools
|
|
375
|
-
if (!tool || typeof tool !== 'object')
|
|
376
|
-
continue;
|
|
377
|
-
const enabled = tool.enabled;
|
|
378
|
-
if (enabled === false)
|
|
379
|
-
continue;
|
|
380
|
-
const description = typeof tool.description === 'string'
|
|
381
|
-
? tool.description
|
|
382
|
-
: undefined;
|
|
383
|
-
out.push({ name, description });
|
|
384
|
-
}
|
|
385
|
-
return out;
|
|
386
|
-
}
|
|
387
|
-
catch {
|
|
388
|
-
return [];
|
|
389
|
-
}
|
|
390
|
-
}
|
|
391
|
-
// Tiny non-crypto hash — the value only needs to detect change. djb2 variant.
|
|
392
|
-
function simpleHash(str) {
|
|
393
|
-
let h = 5381;
|
|
394
|
-
for (let i = 0; i < str.length; i++) {
|
|
395
|
-
h = ((h << 5) + h + str.charCodeAt(i)) | 0;
|
|
396
|
-
}
|
|
397
|
-
return String(h);
|
|
398
|
-
}
|
|
399
|
-
class ProvisionError extends Error {
|
|
400
|
-
status;
|
|
401
|
-
constructor(status, body) {
|
|
402
|
-
super(`HTTP ${status} ${body}`.slice(0, 240));
|
|
403
|
-
this.status = status;
|
|
404
|
-
}
|
|
405
|
-
}
|
|
406
|
-
function textResult(text) {
|
|
407
|
-
return { content: [{ type: 'text', text }] };
|
|
408
|
-
}
|
|
409
|
-
function errorResult(text) {
|
|
410
|
-
return { content: [{ type: 'text', text }], isError: true };
|
|
411
|
-
}
|
package/dist/proxy.d.ts
DELETED
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Remote MCP tool-call proxy — Streamable HTTP.
|
|
3
|
-
*
|
|
4
|
-
* Used by within__use to invoke a tool on another vendor's MCP server.
|
|
5
|
-
*
|
|
6
|
-
* The MCP Streamable HTTP transport requires a handshake before tool calls:
|
|
7
|
-
* 1. POST /mcp { method: "initialize", ... } → 200 with mcp-session-id header
|
|
8
|
-
* 2. POST /mcp { method: "notifications/initialized" } (with that session id)
|
|
9
|
-
* 3. POST /mcp { method: "tools/call", ... } (with that session id) → result
|
|
10
|
-
*
|
|
11
|
-
* Skipping steps 1+2 yields "Server not initialized" on the target server,
|
|
12
|
-
* which is what killed within__use against the real Beacon CRM deployment.
|
|
13
|
-
*
|
|
14
|
-
* Response bodies come back as either application/json (a single JSON-RPC
|
|
15
|
-
* response) or text/event-stream (SSE with one or more `event: message \n
|
|
16
|
-
* data: {...}` blocks). We accept both and extract the first JSON-RPC
|
|
17
|
-
* response message we see.
|
|
18
|
-
*
|
|
19
|
-
* Session reuse: we cache the session id per (url, token) so a subsequent
|
|
20
|
-
* within__use to the same target doesn't re-handshake. Cache keyed on access
|
|
21
|
-
* token which itself has a short TTL, so there's no long-lived drift risk.
|
|
22
|
-
*/
|
|
23
|
-
export interface RemoteToolResult {
|
|
24
|
-
ok: boolean;
|
|
25
|
-
status: number;
|
|
26
|
-
body: unknown;
|
|
27
|
-
}
|
|
28
|
-
export declare function callRemoteMcpTool(mcpServerUrl: string, accessToken: string, toolName: string, toolArguments: Record<string, unknown>, options?: {
|
|
29
|
-
timeoutMs?: number;
|
|
30
|
-
requestId?: string | number;
|
|
31
|
-
}): Promise<RemoteToolResult>;
|
package/dist/proxy.js
DELETED
|
@@ -1,158 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Remote MCP tool-call proxy — Streamable HTTP.
|
|
3
|
-
*
|
|
4
|
-
* Used by within__use to invoke a tool on another vendor's MCP server.
|
|
5
|
-
*
|
|
6
|
-
* The MCP Streamable HTTP transport requires a handshake before tool calls:
|
|
7
|
-
* 1. POST /mcp { method: "initialize", ... } → 200 with mcp-session-id header
|
|
8
|
-
* 2. POST /mcp { method: "notifications/initialized" } (with that session id)
|
|
9
|
-
* 3. POST /mcp { method: "tools/call", ... } (with that session id) → result
|
|
10
|
-
*
|
|
11
|
-
* Skipping steps 1+2 yields "Server not initialized" on the target server,
|
|
12
|
-
* which is what killed within__use against the real Beacon CRM deployment.
|
|
13
|
-
*
|
|
14
|
-
* Response bodies come back as either application/json (a single JSON-RPC
|
|
15
|
-
* response) or text/event-stream (SSE with one or more `event: message \n
|
|
16
|
-
* data: {...}` blocks). We accept both and extract the first JSON-RPC
|
|
17
|
-
* response message we see.
|
|
18
|
-
*
|
|
19
|
-
* Session reuse: we cache the session id per (url, token) so a subsequent
|
|
20
|
-
* within__use to the same target doesn't re-handshake. Cache keyed on access
|
|
21
|
-
* token which itself has a short TTL, so there's no long-lived drift risk.
|
|
22
|
-
*/
|
|
23
|
-
const sessionCache = new Map();
|
|
24
|
-
const SESSION_TTL_MS = 9 * 60_000; // generous buffer under the 10-min access-token TTL
|
|
25
|
-
const PROTOCOL_VERSION = '2025-11-25';
|
|
26
|
-
export async function callRemoteMcpTool(mcpServerUrl, accessToken, toolName, toolArguments, options) {
|
|
27
|
-
const timeoutMs = options?.timeoutMs ?? 30_000;
|
|
28
|
-
const controller = new AbortController();
|
|
29
|
-
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
30
|
-
try {
|
|
31
|
-
const sessionId = await ensureSession(mcpServerUrl, accessToken, controller.signal);
|
|
32
|
-
const res = await fetch(mcpServerUrl, {
|
|
33
|
-
method: 'POST',
|
|
34
|
-
headers: {
|
|
35
|
-
'Content-Type': 'application/json',
|
|
36
|
-
Accept: 'application/json, text/event-stream',
|
|
37
|
-
Authorization: `Bearer ${accessToken}`,
|
|
38
|
-
'mcp-session-id': sessionId,
|
|
39
|
-
},
|
|
40
|
-
body: JSON.stringify({
|
|
41
|
-
jsonrpc: '2.0',
|
|
42
|
-
id: options?.requestId ?? 1,
|
|
43
|
-
method: 'tools/call',
|
|
44
|
-
params: { name: toolName, arguments: toolArguments },
|
|
45
|
-
}),
|
|
46
|
-
signal: controller.signal,
|
|
47
|
-
});
|
|
48
|
-
const text = await res.text();
|
|
49
|
-
const parsed = parseJsonRpcResponse(text, res.headers.get('content-type') ?? '');
|
|
50
|
-
// If the target says our session has vanished (container recycled,
|
|
51
|
-
// memory eviction, etc.) drop our cache entry and retry exactly once.
|
|
52
|
-
if (!res.ok && looksLikeStaleSession(text)) {
|
|
53
|
-
sessionCache.delete(cacheKey(mcpServerUrl, accessToken));
|
|
54
|
-
return callRemoteMcpTool(mcpServerUrl, accessToken, toolName, toolArguments, options);
|
|
55
|
-
}
|
|
56
|
-
return { ok: res.ok, status: res.status, body: parsed ?? text };
|
|
57
|
-
}
|
|
58
|
-
finally {
|
|
59
|
-
clearTimeout(timer);
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
function cacheKey(url, token) {
|
|
63
|
-
return `${url}::${token}`;
|
|
64
|
-
}
|
|
65
|
-
function looksLikeStaleSession(body) {
|
|
66
|
-
return /session\s+(expired|not\s+found|invalid)/i.test(body) || body.includes('Session expired');
|
|
67
|
-
}
|
|
68
|
-
async function ensureSession(url, accessToken, signal) {
|
|
69
|
-
const key = cacheKey(url, accessToken);
|
|
70
|
-
const cached = sessionCache.get(key);
|
|
71
|
-
if (cached && Date.now() - cached.createdAt < SESSION_TTL_MS) {
|
|
72
|
-
return cached.sessionId;
|
|
73
|
-
}
|
|
74
|
-
const initRes = await fetch(url, {
|
|
75
|
-
method: 'POST',
|
|
76
|
-
headers: {
|
|
77
|
-
'Content-Type': 'application/json',
|
|
78
|
-
Accept: 'application/json, text/event-stream',
|
|
79
|
-
Authorization: `Bearer ${accessToken}`,
|
|
80
|
-
},
|
|
81
|
-
body: JSON.stringify({
|
|
82
|
-
jsonrpc: '2.0',
|
|
83
|
-
id: 0,
|
|
84
|
-
method: 'initialize',
|
|
85
|
-
params: {
|
|
86
|
-
protocolVersion: PROTOCOL_VERSION,
|
|
87
|
-
capabilities: {},
|
|
88
|
-
clientInfo: { name: 'within-mcp-auth/proxy', version: '0.1.0' },
|
|
89
|
-
},
|
|
90
|
-
}),
|
|
91
|
-
signal,
|
|
92
|
-
});
|
|
93
|
-
if (!initRes.ok) {
|
|
94
|
-
const errText = await initRes.text();
|
|
95
|
-
throw new Error(`Remote MCP initialize failed (${initRes.status}): ${errText.slice(0, 240)}`);
|
|
96
|
-
}
|
|
97
|
-
const sessionId = initRes.headers.get('mcp-session-id');
|
|
98
|
-
if (!sessionId) {
|
|
99
|
-
throw new Error('Remote MCP server did not return an mcp-session-id header on initialize');
|
|
100
|
-
}
|
|
101
|
-
// Consume init response body to free the socket before the next POST.
|
|
102
|
-
await initRes.text().catch(() => undefined);
|
|
103
|
-
// Fire-and-forget the initialized notification. The spec wants it, and
|
|
104
|
-
// some servers block tools/call until they've seen it.
|
|
105
|
-
const notifRes = await fetch(url, {
|
|
106
|
-
method: 'POST',
|
|
107
|
-
headers: {
|
|
108
|
-
'Content-Type': 'application/json',
|
|
109
|
-
Accept: 'application/json, text/event-stream',
|
|
110
|
-
Authorization: `Bearer ${accessToken}`,
|
|
111
|
-
'mcp-session-id': sessionId,
|
|
112
|
-
},
|
|
113
|
-
body: JSON.stringify({
|
|
114
|
-
jsonrpc: '2.0',
|
|
115
|
-
method: 'notifications/initialized',
|
|
116
|
-
}),
|
|
117
|
-
signal,
|
|
118
|
-
});
|
|
119
|
-
// 202 Accepted is the expected response for notifications; anything else
|
|
120
|
-
// we tolerate (target may acknowledge with 200 or drop it). We do NOT
|
|
121
|
-
// throw here because some implementations don't require this step.
|
|
122
|
-
await notifRes.text().catch(() => undefined);
|
|
123
|
-
sessionCache.set(key, { sessionId, createdAt: Date.now() });
|
|
124
|
-
return sessionId;
|
|
125
|
-
}
|
|
126
|
-
/**
|
|
127
|
-
* The MCP StreamableHTTP transport can respond with either raw JSON or SSE.
|
|
128
|
-
* This function peels off the SSE framing (if present) and returns the
|
|
129
|
-
* first JSON-RPC response object it finds, or null if the body is neither.
|
|
130
|
-
*/
|
|
131
|
-
function parseJsonRpcResponse(text, contentType) {
|
|
132
|
-
const trimmed = text.trim();
|
|
133
|
-
if (!trimmed)
|
|
134
|
-
return null;
|
|
135
|
-
if (contentType.includes('text/event-stream') || trimmed.startsWith('event:')) {
|
|
136
|
-
// SSE frame format: event: message\ndata: {...}\n\n
|
|
137
|
-
for (const line of trimmed.split(/\r?\n/)) {
|
|
138
|
-
if (line.startsWith('data:')) {
|
|
139
|
-
const payload = line.slice(5).trim();
|
|
140
|
-
if (!payload)
|
|
141
|
-
continue;
|
|
142
|
-
try {
|
|
143
|
-
return JSON.parse(payload);
|
|
144
|
-
}
|
|
145
|
-
catch {
|
|
146
|
-
// Not JSON, try next line
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
return null;
|
|
151
|
-
}
|
|
152
|
-
try {
|
|
153
|
-
return JSON.parse(trimmed);
|
|
154
|
-
}
|
|
155
|
-
catch {
|
|
156
|
-
return null;
|
|
157
|
-
}
|
|
158
|
-
}
|
package/dist/validate.d.ts
DELETED
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* With.in MCP Auth SDK — Token Validation
|
|
3
|
-
*
|
|
4
|
-
* Validates tokens against With.in's JWKS endpoint.
|
|
5
|
-
* Caches the JWKS keys in memory with a 5-minute refresh interval.
|
|
6
|
-
*
|
|
7
|
-
* SYNC NOTE: vendor/lib/validate.ts intentionally diverges here — it adds
|
|
8
|
-
* `ngrok-skip-browser-warning` headers for local-dev only. Keep the
|
|
9
|
-
* published SDK clean of those.
|
|
10
|
-
*/
|
|
11
|
-
import type { WithinTokenClaims } from './types.js';
|
|
12
|
-
export declare function validateWithinToken(token: string, jwksUrl: string): Promise<WithinTokenClaims | null>;
|
|
13
|
-
export declare function extractBearerToken(authHeader?: string): string | null;
|
|
14
|
-
/** Report to With.in that the vendor recognises this user as an existing customer.
|
|
15
|
-
* Call this when a With.in token arrives and you find the user in your own DB.
|
|
16
|
-
* With.in will mark the user as a subscriber (no conversion fee).
|
|
17
|
-
*
|
|
18
|
-
* Pass `withinAccessToken` whenever it's available — the backend authenticates
|
|
19
|
-
* the request by checking that token's claims match (email + vendor_slug).
|
|
20
|
-
* Without it the call is rejected, so the vendorTokenValidator path (where
|
|
21
|
-
* no With.in token exists) cannot drive conversions. That path is intentionally
|
|
22
|
-
* niche today; broader auth (vendor shared secret) is planned. */
|
|
23
|
-
export declare function reportConversion(authServerUrl: string, vendorSlug: string, email: string, withinAccessToken?: string): Promise<void>;
|
|
24
|
-
/** Check whether the token holder is now a subscriber for this vendor.
|
|
25
|
-
* Used to unstick users who paid seconds ago but whose cached access
|
|
26
|
-
* token still says the trial is exhausted. Returns false on any error —
|
|
27
|
-
* falling back to the normal trial-exhausted message is the safe default. */
|
|
28
|
-
export declare function checkSubscriptionStatus(authServerUrl: string, token: string): Promise<boolean>;
|
|
29
|
-
export interface ReportToolCallResult {
|
|
30
|
-
/** Backend returned 401 token_revoked — caller must block the tool call.
|
|
31
|
-
* An access token's JWT signature stays valid until its 10-min TTL, so
|
|
32
|
-
* local validation alone can't catch a dashboard-initiated revoke. The
|
|
33
|
-
* /oauth/report roundtrip is the enforcement point. */
|
|
34
|
-
revoked: boolean;
|
|
35
|
-
/** Backend returned 429 tool_budget_exhausted — vendor has configured a
|
|
36
|
-
* per-tool call budget and the user has hit it. Caller must block the
|
|
37
|
-
* tool call so the vendor's handler does NOT run. The user can still
|
|
38
|
-
* call other tools (budgets are per-tool, not global). */
|
|
39
|
-
budgetExhausted?: {
|
|
40
|
-
tool: string;
|
|
41
|
-
limit: number;
|
|
42
|
-
};
|
|
43
|
-
}
|
|
44
|
-
/** Report a tool call back to With.in for lead tracking and revocation
|
|
45
|
-
* enforcement. `aiClient` — the MCP client name captured from the session's
|
|
46
|
-
* `initialize` request (`clientInfo.name`). Lets the vendor and the user
|
|
47
|
-
* see which agent (Claude Desktop, Cursor, ChatGPT, …) actually made
|
|
48
|
-
* the call.
|
|
49
|
-
*
|
|
50
|
-
* Returns `{ revoked: true }` when the backend reports the token has been
|
|
51
|
-
* revoked since issuance. Network/server errors return `{ revoked: false }`
|
|
52
|
-
* so reporting outages never block a tool call. */
|
|
53
|
-
export declare function reportToolCall(authServerUrl: string, token: string, vendorSlug: string, toolName: string, aiClient?: string, toolArguments?: Record<string, unknown> | null): Promise<ReportToolCallResult>;
|