@tradejs/app 3.1.29-beta.270 → 3.1.29-beta.272

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.
@@ -18,7 +18,6 @@ export const Sidebar = () => {
18
18
  const pathname = usePathname();
19
19
 
20
20
  const navItems = [
21
- { icon: FiLink, label: 'MCP connections', path: '/routes/mcp' },
22
21
  {
23
22
  icon: FiBarChart2,
24
23
  label: 'Dashboard',
@@ -76,6 +75,15 @@ export const Sidebar = () => {
76
75
 
77
76
  <VStack gap={2}>
78
77
  <AccountSettingsDrawer />
78
+ <IconButton
79
+ aria-label="MCP connections"
80
+ size="md"
81
+ colorPalette="teal"
82
+ variant={pathname.includes('/routes/mcp') ? 'solid' : 'outline'}
83
+ onClick={() => router.push('/routes/mcp')}
84
+ >
85
+ <FiLink />
86
+ </IconButton>
79
87
  <IconButton
80
88
  aria-label="Sign out"
81
89
  size="md"
@@ -14,6 +14,34 @@ import { createTradejsMcpServer } from '#app/lib/mcp/server';
14
14
  export const runtime = 'nodejs';
15
15
  export const dynamic = 'force-dynamic';
16
16
 
17
+ // Stateless clients may initialize and list tools for each artifact chunk.
18
+ // Account for that traffic separately so downloads cannot exhaust normal reads.
19
+ const requestBudget = (body: string, scopes: readonly string[]) => {
20
+ try {
21
+ const message = JSON.parse(body);
22
+ if (message?.jsonrpc === '2.0') {
23
+ if (
24
+ message.method === 'tools/call' &&
25
+ message.params?.name === 'artifact_get' &&
26
+ scopes.includes('diagnostics:read')
27
+ )
28
+ return { bucket: 'artifact-requests', maximum: 600 };
29
+ if (
30
+ [
31
+ 'initialize',
32
+ 'notifications/initialized',
33
+ 'tools/list',
34
+ 'ping',
35
+ ].includes(message.method)
36
+ )
37
+ return { bucket: 'protocol-requests', maximum: 1200 };
38
+ }
39
+ } catch {
40
+ // Invalid JSON still consumes the ordinary request budget.
41
+ }
42
+ return { bucket: 'requests', maximum: 120 };
43
+ };
44
+
17
45
  export const POST = async (request: Request) => {
18
46
  if (!mcpEnabled()) return oauthResponse({ error: 'MCP disabled' }, 503);
19
47
  const origin = request.headers.get('origin');
@@ -25,19 +53,18 @@ export const POST = async (request: Request) => {
25
53
  return oauthResponse({ error: 'Unauthorized' }, 401, {
26
54
  'WWW-Authenticate': `Bearer resource_metadata="${mcpOrigin()}/.well-known/oauth-protected-resource/mcp"`,
27
55
  });
56
+ const body = await readMcpBody(request, 65536);
57
+ const budget = requestBudget(body, principal.scopes);
28
58
  if (
29
59
  !(await mcpStorage.rateLimit(
30
- `requests:${fingerprint(`${principal.userName}:${principal.clientId}`)}`,
31
- 120,
60
+ `${budget.bucket}:${fingerprint(`${principal.userName}:${principal.clientId}`)}`,
61
+ budget.maximum,
32
62
  60,
33
63
  ))
34
64
  )
35
65
  return oauthResponse({ error: 'Rate limit exceeded' }, 429, {
36
66
  'Retry-After': '60',
37
67
  });
38
- const body = await readMcpBody(request, 65536);
39
- if (Buffer.byteLength(body) > 65536)
40
- return oauthResponse({ error: 'Request too large' }, 413);
41
68
  const transport = new WebStandardStreamableHTTPServerTransport({
42
69
  sessionIdGenerator: undefined,
43
70
  enableJsonResponse: true,