create-chaaskit 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.
Files changed (122) hide show
  1. package/dist/cli.d.ts +3 -0
  2. package/dist/cli.d.ts.map +1 -0
  3. package/dist/cli.js +25 -0
  4. package/dist/cli.js.map +1 -0
  5. package/dist/commands/add-infra.d.ts +6 -0
  6. package/dist/commands/add-infra.d.ts.map +1 -0
  7. package/dist/commands/add-infra.js +160 -0
  8. package/dist/commands/add-infra.js.map +1 -0
  9. package/dist/commands/build.d.ts +2 -0
  10. package/dist/commands/build.d.ts.map +1 -0
  11. package/dist/commands/build.js +63 -0
  12. package/dist/commands/build.js.map +1 -0
  13. package/dist/commands/db-sync.d.ts +13 -0
  14. package/dist/commands/db-sync.d.ts.map +1 -0
  15. package/dist/commands/db-sync.js +108 -0
  16. package/dist/commands/db-sync.js.map +1 -0
  17. package/dist/commands/dev.d.ts +7 -0
  18. package/dist/commands/dev.d.ts.map +1 -0
  19. package/dist/commands/dev.js +61 -0
  20. package/dist/commands/dev.js.map +1 -0
  21. package/dist/commands/init.d.ts +9 -0
  22. package/dist/commands/init.d.ts.map +1 -0
  23. package/dist/commands/init.js +214 -0
  24. package/dist/commands/init.js.map +1 -0
  25. package/dist/index.d.ts +3 -0
  26. package/dist/index.d.ts.map +1 -0
  27. package/dist/index.js +57 -0
  28. package/dist/index.js.map +1 -0
  29. package/dist/templates/.env.example +24 -0
  30. package/dist/templates/README.md +81 -0
  31. package/dist/templates/app/components/AcceptInviteClient.tsx +10 -0
  32. package/dist/templates/app/components/AdminDashboardClient.tsx +10 -0
  33. package/dist/templates/app/components/AdminTeamClient.tsx +10 -0
  34. package/dist/templates/app/components/AdminTeamsClient.tsx +10 -0
  35. package/dist/templates/app/components/AdminUsersClient.tsx +10 -0
  36. package/dist/templates/app/components/ApiKeysClient.tsx +10 -0
  37. package/dist/templates/app/components/AutomationsClient.tsx +10 -0
  38. package/dist/templates/app/components/ChatClient.tsx +13 -0
  39. package/dist/templates/app/components/ClientOnly.tsx +6 -0
  40. package/dist/templates/app/components/DocumentsClient.tsx +10 -0
  41. package/dist/templates/app/components/OAuthConsentClient.tsx +10 -0
  42. package/dist/templates/app/components/PricingClient.tsx +10 -0
  43. package/dist/templates/app/components/TeamSettingsClient.tsx +10 -0
  44. package/dist/templates/app/components/VerifyEmailClient.tsx +10 -0
  45. package/dist/templates/app/entry.client.tsx +12 -0
  46. package/dist/templates/app/entry.server.tsx +67 -0
  47. package/dist/templates/app/root.tsx +91 -0
  48. package/dist/templates/app/routes/_index.tsx +82 -0
  49. package/dist/templates/app/routes/admin._index.tsx +57 -0
  50. package/dist/templates/app/routes/admin.teams.$teamId.tsx +57 -0
  51. package/dist/templates/app/routes/admin.teams._index.tsx +57 -0
  52. package/dist/templates/app/routes/admin.users.tsx +57 -0
  53. package/dist/templates/app/routes/api-keys.tsx +57 -0
  54. package/dist/templates/app/routes/automations.tsx +57 -0
  55. package/dist/templates/app/routes/chat._index.tsx +11 -0
  56. package/dist/templates/app/routes/chat.admin._index.tsx +10 -0
  57. package/dist/templates/app/routes/chat.admin.teams.$teamId.tsx +10 -0
  58. package/dist/templates/app/routes/chat.admin.teams._index.tsx +10 -0
  59. package/dist/templates/app/routes/chat.admin.users.tsx +10 -0
  60. package/dist/templates/app/routes/chat.api-keys.tsx +10 -0
  61. package/dist/templates/app/routes/chat.automations.tsx +10 -0
  62. package/dist/templates/app/routes/chat.documents.tsx +10 -0
  63. package/dist/templates/app/routes/chat.team.$teamId.settings.tsx +10 -0
  64. package/dist/templates/app/routes/chat.thread.$threadId.tsx +11 -0
  65. package/dist/templates/app/routes/chat.tsx +39 -0
  66. package/dist/templates/app/routes/documents.tsx +57 -0
  67. package/dist/templates/app/routes/invite.$token.tsx +10 -0
  68. package/dist/templates/app/routes/login.tsx +334 -0
  69. package/dist/templates/app/routes/oauth.consent.tsx +10 -0
  70. package/dist/templates/app/routes/pricing.tsx +10 -0
  71. package/dist/templates/app/routes/privacy.tsx +197 -0
  72. package/dist/templates/app/routes/register.tsx +398 -0
  73. package/dist/templates/app/routes/shared.$shareId.tsx +226 -0
  74. package/dist/templates/app/routes/team.$teamId.settings.tsx +57 -0
  75. package/dist/templates/app/routes/terms.tsx +173 -0
  76. package/dist/templates/app/routes/thread.$threadId.tsx +102 -0
  77. package/dist/templates/app/routes/verify-email.tsx +10 -0
  78. package/dist/templates/app/routes.ts +47 -0
  79. package/dist/templates/config/app.config.ts +216 -0
  80. package/dist/templates/docs/admin.md +257 -0
  81. package/dist/templates/docs/api-keys.md +403 -0
  82. package/dist/templates/docs/authentication.md +247 -0
  83. package/dist/templates/docs/configuration.md +1212 -0
  84. package/dist/templates/docs/custom-pages.md +466 -0
  85. package/dist/templates/docs/deployment.md +362 -0
  86. package/dist/templates/docs/development.md +411 -0
  87. package/dist/templates/docs/documents.md +293 -0
  88. package/dist/templates/docs/extensions.md +639 -0
  89. package/dist/templates/docs/index.md +139 -0
  90. package/dist/templates/docs/installation.md +286 -0
  91. package/dist/templates/docs/mcp.md +952 -0
  92. package/dist/templates/docs/native-tools.md +688 -0
  93. package/dist/templates/docs/queue.md +514 -0
  94. package/dist/templates/docs/scheduled-prompts.md +279 -0
  95. package/dist/templates/docs/settings.md +415 -0
  96. package/dist/templates/docs/slack.md +318 -0
  97. package/dist/templates/docs/styling.md +288 -0
  98. package/dist/templates/extensions/agents/.gitkeep +0 -0
  99. package/dist/templates/extensions/pages/.gitkeep +0 -0
  100. package/dist/templates/extensions/payment-plans/.gitkeep +0 -0
  101. package/dist/templates/index.html +16 -0
  102. package/dist/templates/infra-aws/.github/workflows/deploy.yml +95 -0
  103. package/dist/templates/infra-aws/README.md +207 -0
  104. package/dist/templates/infra-aws/bin/cdk.ts +18 -0
  105. package/dist/templates/infra-aws/cdk.json +43 -0
  106. package/dist/templates/infra-aws/config/deployment.ts +156 -0
  107. package/dist/templates/infra-aws/lib/chaaskit-stack.ts +419 -0
  108. package/dist/templates/infra-aws/package.json +27 -0
  109. package/dist/templates/infra-aws/scripts/build-app.sh +63 -0
  110. package/dist/templates/infra-aws/tsconfig.json +25 -0
  111. package/dist/templates/package.json +46 -0
  112. package/dist/templates/prisma/schema/base.prisma +584 -0
  113. package/dist/templates/prisma/schema/custom.prisma +24 -0
  114. package/dist/templates/prisma/schema.prisma +271 -0
  115. package/dist/templates/public/favicon.svg +4 -0
  116. package/dist/templates/public/logo.svg +4 -0
  117. package/dist/templates/react-router.config.ts +11 -0
  118. package/dist/templates/server.js +52 -0
  119. package/dist/templates/src/main.tsx +8 -0
  120. package/dist/templates/tsconfig.json +26 -0
  121. package/dist/templates/vite.config.ts +26 -0
  122. package/package.json +46 -0
@@ -0,0 +1,688 @@
1
+ # Native Tools
2
+
3
+ Native tools are built-in tools that run directly in your application, without requiring external MCP servers. They provide capabilities like web scraping, file processing, or custom integrations that are part of your core server.
4
+
5
+ ## Overview
6
+
7
+ Unlike MCP tools (which connect to external servers), native tools:
8
+ - Run in the same process as your server
9
+ - Have no external dependencies
10
+ - Are opt-in per agent (must be explicitly enabled)
11
+ - Use the `native:` prefix in configuration
12
+
13
+ ## Available Native Tools
14
+
15
+ | Tool | Description | Widget |
16
+ |------|-------------|--------|
17
+ | `web-scrape` | Fetches a URL and returns the content as plain text. Supports HTML, JSON, and plain text responses. | No |
18
+ | `get-plan-usage` | Returns the current user's plan information and usage statistics. | Yes |
19
+
20
+ ## Configuration
21
+
22
+ Native tools are enabled per-agent using the `allowedTools` configuration:
23
+
24
+ ```typescript
25
+ // config/app.config.ts
26
+ agent: {
27
+ agents: [
28
+ {
29
+ id: 'general',
30
+ name: 'General Assistant',
31
+ provider: 'openai',
32
+ model: 'gpt-4o-mini',
33
+ systemPrompt: 'You are a helpful assistant.',
34
+ maxTokens: 4096,
35
+ isDefault: true,
36
+ // No allowedTools = all MCP tools, but NO native tools
37
+ },
38
+ {
39
+ id: 'research-assistant',
40
+ name: 'Research Assistant',
41
+ provider: 'openai',
42
+ model: 'gpt-4o',
43
+ systemPrompt: 'You are a research assistant that can fetch and analyze web content.',
44
+ maxTokens: 8192,
45
+ // Enable native web-scrape tool + all tools from a specific MCP server
46
+ allowedTools: ['native:web-scrape', 'search-server:*'],
47
+ },
48
+ ],
49
+ }
50
+ ```
51
+
52
+ ### Tool Pattern Syntax
53
+
54
+ - `native:*` - All native tools
55
+ - `native:tool-name` - Specific native tool (e.g., `native:web-scrape`)
56
+ - `mcp-server:*` - All tools from an MCP server
57
+ - `mcp-server:tool-name` - Specific tool from an MCP server
58
+
59
+ ### Default Behavior
60
+
61
+ - **No `allowedTools`**: Agent has access to all MCP tools but NO native tools
62
+ - **With `allowedTools`**: Agent only has access to tools matching the specified patterns
63
+
64
+ ## Creating New Native Tools
65
+
66
+ Native tools are defined in `packages/server/src/tools/`. Each tool is a module that exports a `NativeTool` object.
67
+
68
+ ### Step 1: Create the Tool File
69
+
70
+ Create a new file in `packages/server/src/tools/`:
71
+
72
+ ```typescript
73
+ // packages/server/src/tools/my-tool.ts
74
+ import type { NativeTool, ToolResult, ToolContext } from './types.js';
75
+
76
+ export const myTool: NativeTool = {
77
+ name: 'my-tool',
78
+
79
+ description: 'A description of what this tool does. This is shown to the LLM.',
80
+
81
+ inputSchema: {
82
+ type: 'object',
83
+ properties: {
84
+ param1: {
85
+ type: 'string',
86
+ description: 'Description of param1',
87
+ },
88
+ param2: {
89
+ type: 'number',
90
+ description: 'Description of param2',
91
+ default: 10,
92
+ },
93
+ },
94
+ required: ['param1'],
95
+ },
96
+
97
+ async execute(input: Record<string, unknown>, context: ToolContext): Promise<ToolResult> {
98
+ const param1 = input.param1 as string;
99
+ const param2 = (input.param2 as number) || 10;
100
+
101
+ try {
102
+ // Your tool logic here
103
+ const result = await doSomething(param1, param2);
104
+
105
+ return {
106
+ content: [{ type: 'text', text: result }],
107
+ };
108
+ } catch (error) {
109
+ const message = error instanceof Error ? error.message : 'Unknown error';
110
+ return {
111
+ content: [{ type: 'text', text: `Error: ${message}` }],
112
+ isError: true,
113
+ };
114
+ }
115
+ },
116
+ };
117
+ ```
118
+
119
+ ### Step 2: Register the Tool
120
+
121
+ Add your tool to the registry in `packages/server/src/tools/index.ts`:
122
+
123
+ ```typescript
124
+ import { myTool } from './my-tool.js';
125
+
126
+ // Register built-in native tools
127
+ nativeToolRegistry.set('web-scrape', webScrapeTool);
128
+ nativeToolRegistry.set('my-tool', myTool); // Add this line
129
+ ```
130
+
131
+ ### Step 3: Configure Agent Access
132
+
133
+ Enable the tool for agents that should have access:
134
+
135
+ ```typescript
136
+ // config/app.config.ts
137
+ {
138
+ id: 'my-agent',
139
+ name: 'My Agent',
140
+ // ...
141
+ allowedTools: ['native:my-tool'],
142
+ }
143
+ ```
144
+
145
+ ## Type Definitions
146
+
147
+ ### NativeTool
148
+
149
+ ```typescript
150
+ interface NativeTool {
151
+ /** Unique tool name (used in allowedTools as 'native:name') */
152
+ name: string;
153
+
154
+ /** Human-readable description for the LLM */
155
+ description: string;
156
+
157
+ /** JSON Schema describing the tool's input parameters */
158
+ inputSchema: JSONSchema;
159
+
160
+ /** Optional metadata for UI templates and other extensions */
161
+ _meta?: NativeToolMeta;
162
+
163
+ /** Execute the tool with the given input */
164
+ execute: (input: Record<string, unknown>, context: ToolContext) => Promise<ToolResult>;
165
+ }
166
+ ```
167
+
168
+ ### NativeToolMeta
169
+
170
+ Metadata for configuring tool widgets:
171
+
172
+ ```typescript
173
+ interface NativeToolMeta {
174
+ /** Inline HTML template for widget rendering */
175
+ 'ui/template'?: string;
176
+
177
+ /** File path relative to tools/templates/ for widget rendering */
178
+ 'ui/templateFile'?: string;
179
+
180
+ /** Additional metadata keys */
181
+ [key: string]: unknown;
182
+ }
183
+ ```
184
+
185
+ ### ToolContext
186
+
187
+ The context object passed to every tool execution:
188
+
189
+ ```typescript
190
+ interface ToolContext {
191
+ userId?: string; // Current user's ID (if authenticated)
192
+ threadId?: string; // Current thread ID
193
+ agentId?: string; // Current agent ID
194
+ }
195
+ ```
196
+
197
+ ### ToolResult
198
+
199
+ The result returned from tool execution:
200
+
201
+ ```typescript
202
+ interface ToolResult {
203
+ content: MCPContentItem[]; // Array of content items (text, images, etc.)
204
+ isError?: boolean; // Set to true if the tool encountered an error
205
+ structuredContent?: Record<string, unknown>; // Structured data for widget rendering
206
+ }
207
+
208
+ // Content item types
209
+ type MCPContentItem =
210
+ | { type: 'text'; text: string }
211
+ | { type: 'image'; data: string; mimeType: string }
212
+ | { type: 'resource'; resource: { uri: string; text?: string; blob?: string } };
213
+ ```
214
+
215
+ ## Example: Web Scrape Tool
216
+
217
+ Here's the built-in `web-scrape` tool as a reference implementation:
218
+
219
+ ```typescript
220
+ // packages/server/src/tools/web-scrape.ts
221
+ export const webScrapeTool: NativeTool = {
222
+ name: 'web-scrape',
223
+
224
+ description: 'Fetches the content of a web page and returns it as plain text.',
225
+
226
+ inputSchema: {
227
+ type: 'object',
228
+ properties: {
229
+ url: {
230
+ type: 'string',
231
+ description: 'The URL of the web page to fetch',
232
+ },
233
+ maxLength: {
234
+ type: 'number',
235
+ description: 'Maximum characters to return (default: 50000)',
236
+ default: 50000,
237
+ },
238
+ },
239
+ required: ['url'],
240
+ },
241
+
242
+ async execute(input: Record<string, unknown>, _context: ToolContext): Promise<ToolResult> {
243
+ const url = input.url as string;
244
+ const maxLength = (input.maxLength as number) || 50000;
245
+
246
+ // Validate URL
247
+ let parsedUrl: URL;
248
+ try {
249
+ parsedUrl = new URL(url);
250
+ } catch {
251
+ return {
252
+ content: [{ type: 'text', text: `Invalid URL: ${url}` }],
253
+ isError: true,
254
+ };
255
+ }
256
+
257
+ // Only allow http/https
258
+ if (!['http:', 'https:'].includes(parsedUrl.protocol)) {
259
+ return {
260
+ content: [{ type: 'text', text: `Invalid protocol: ${parsedUrl.protocol}` }],
261
+ isError: true,
262
+ };
263
+ }
264
+
265
+ try {
266
+ const response = await fetch(url, {
267
+ headers: {
268
+ 'User-Agent': 'Mozilla/5.0 (compatible; ChatBot/1.0)',
269
+ },
270
+ });
271
+
272
+ if (!response.ok) {
273
+ return {
274
+ content: [{ type: 'text', text: `HTTP error: ${response.status}` }],
275
+ isError: true,
276
+ };
277
+ }
278
+
279
+ const html = await response.text();
280
+ const text = htmlToText(html); // Convert HTML to plain text
281
+
282
+ return {
283
+ content: [{ type: 'text', text: truncateText(text, maxLength) }],
284
+ };
285
+ } catch (error) {
286
+ return {
287
+ content: [{ type: 'text', text: `Failed to fetch: ${error.message}` }],
288
+ isError: true,
289
+ };
290
+ }
291
+ },
292
+ };
293
+ ```
294
+
295
+ ## Widget Support
296
+
297
+ Native tools can render rich UI widgets instead of plain text output. This is useful for displaying structured data like charts, forms, or interactive components.
298
+
299
+ ### How Widgets Work
300
+
301
+ 1. Tool returns `structuredContent` with data for the widget
302
+ 2. Tool specifies an HTML template via `_meta`
303
+ 3. Template is rendered in a sandboxed iframe
304
+ 4. Template accesses data via `window.openai` global object
305
+
306
+ ### Creating a Tool with a Widget
307
+
308
+ #### Step 1: Add `_meta` to Your Tool
309
+
310
+ Specify a template using either inline HTML or a file reference:
311
+
312
+ ```typescript
313
+ // Using a template file (recommended for complex widgets)
314
+ export const myTool: NativeTool = {
315
+ name: 'my-tool',
316
+ description: 'Does something useful',
317
+ inputSchema: { type: 'object', properties: {}, required: [] },
318
+ _meta: {
319
+ 'ui/templateFile': 'my-tool.html', // Relative to tools/templates/
320
+ },
321
+ async execute(input, context) {
322
+ // ...
323
+ },
324
+ };
325
+
326
+ // Using inline HTML (for simple widgets)
327
+ export const simpleTool: NativeTool = {
328
+ name: 'simple-tool',
329
+ _meta: {
330
+ 'ui/template': '<div id="output"></div><script>document.getElementById("output").textContent = JSON.stringify(window.openai.toolOutput);</script>',
331
+ },
332
+ // ...
333
+ };
334
+ ```
335
+
336
+ #### Step 2: Return `structuredContent`
337
+
338
+ Your tool's `execute` function should return both `content` (for the LLM) and `structuredContent` (for the widget):
339
+
340
+ ```typescript
341
+ async execute(input, context): Promise<ToolResult> {
342
+ const data = await fetchSomeData();
343
+
344
+ return {
345
+ // Text content for the LLM to read
346
+ content: [{ type: 'text', text: `Found ${data.count} items` }],
347
+
348
+ // Structured data for the widget to render
349
+ structuredContent: {
350
+ count: data.count,
351
+ items: data.items,
352
+ timestamp: new Date().toISOString(),
353
+ },
354
+ };
355
+ }
356
+ ```
357
+
358
+ #### Step 3: Create the HTML Template
359
+
360
+ Create your template file in `packages/server/src/tools/templates/`:
361
+
362
+ ```html
363
+ <!-- packages/server/src/tools/templates/my-tool.html -->
364
+ <!DOCTYPE html>
365
+ <html>
366
+ <head>
367
+ <style>
368
+ * { margin: 0; padding: 0; box-sizing: border-box; }
369
+ body {
370
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
371
+ padding: 16px;
372
+ /* Background is automatically set based on theme */
373
+ }
374
+ .card {
375
+ border-radius: 8px;
376
+ padding: 16px;
377
+ background: rgba(99, 102, 241, 0.1);
378
+ }
379
+ .card.dark {
380
+ background: rgba(129, 140, 248, 0.1);
381
+ }
382
+ </style>
383
+ </head>
384
+ <body>
385
+ <div id="card" class="card">
386
+ <div id="content">Loading...</div>
387
+ </div>
388
+
389
+ <script>
390
+ (function() {
391
+ // Access data from window.openai
392
+ const data = window.openai?.toolOutput || {};
393
+ const theme = window.openai?.theme || 'light';
394
+
395
+ // Apply theme
396
+ const card = document.getElementById('card');
397
+ if (theme === 'dark') {
398
+ card.classList.add('dark');
399
+ }
400
+
401
+ // Render content
402
+ document.getElementById('content').textContent = `Found ${data.count} items`;
403
+ })();
404
+ </script>
405
+ </body>
406
+ </html>
407
+ ```
408
+
409
+ ### The `window.openai` API
410
+
411
+ Widgets have access to a global `window.openai` object with the following properties and methods:
412
+
413
+ ```typescript
414
+ window.openai = {
415
+ // Data
416
+ theme: 'light' | 'dark', // Current app theme
417
+ toolInput: { ... }, // Original tool arguments
418
+ toolOutput: { ... }, // structuredContent from tool result
419
+ locale: 'en-US', // Browser locale
420
+ maxHeight: 800, // Maximum render height
421
+
422
+ // Device info
423
+ userAgent: {
424
+ device: { type: 'desktop' | 'mobile' },
425
+ capabilities: { hover: boolean, touch: boolean }
426
+ },
427
+
428
+ // Methods
429
+ openExternal: ({ href }) => void, // Open URL in new tab
430
+ callTool: (name, args) => Promise, // Call another tool (stub)
431
+ sendFollowUpMessage: (args) => Promise, // Send message (stub)
432
+ };
433
+ ```
434
+
435
+ ### Theme Support
436
+
437
+ The app automatically injects theme-appropriate styles into the iframe:
438
+
439
+ - **Light mode**: White background (`#ffffff`)
440
+ - **Dark mode**: Dark background (`#111827`)
441
+
442
+ Your widget should check `window.openai.theme` and apply appropriate styles:
443
+
444
+ ```javascript
445
+ if (window.openai.theme === 'dark') {
446
+ document.body.classList.add('dark');
447
+ // Apply dark mode styles
448
+ }
449
+ ```
450
+
451
+ ### Example: Plan Usage Widget
452
+
453
+ Here's the complete implementation of the `get-plan-usage` tool with a widget:
454
+
455
+ **Tool (`packages/server/src/tools/get-plan-usage.ts`):**
456
+
457
+ ```typescript
458
+ import { db } from '@chaaskit/db';
459
+ import { getConfig } from '../config/loader.js';
460
+ import { getBillingContext } from '../services/usage.js';
461
+ import type { NativeTool } from './types.js';
462
+
463
+ export const getPlanUsageTool: NativeTool = {
464
+ name: 'get-plan-usage',
465
+ description: 'Get the current user\'s plan information and usage statistics.',
466
+ inputSchema: {
467
+ type: 'object',
468
+ properties: {},
469
+ required: [],
470
+ },
471
+ _meta: {
472
+ 'ui/templateFile': 'get-plan-usage.html',
473
+ },
474
+
475
+ async execute(input, context) {
476
+ const config = getConfig();
477
+
478
+ if (!config.payments.enabled) {
479
+ return {
480
+ content: [{ type: 'text', text: 'Payments are not enabled.' }],
481
+ structuredContent: {
482
+ planName: 'Free',
483
+ messagesUsed: 0,
484
+ messageLimit: null,
485
+ credits: null,
486
+ },
487
+ };
488
+ }
489
+
490
+ if (!context.userId) {
491
+ return {
492
+ content: [{ type: 'text', text: 'User not authenticated.' }],
493
+ isError: true,
494
+ };
495
+ }
496
+
497
+ const billingContext = await getBillingContext(context.userId);
498
+ if (!billingContext) {
499
+ return {
500
+ content: [{ type: 'text', text: 'Unable to retrieve billing info.' }],
501
+ isError: true,
502
+ };
503
+ }
504
+
505
+ const planConfig = config.payments.plans.find(p => p.id === billingContext.plan);
506
+ const planName = planConfig?.name || billingContext.plan;
507
+
508
+ return {
509
+ content: [{
510
+ type: 'text',
511
+ text: `Plan: ${planName}\nMessages: ${billingContext.messagesThisMonth}/${billingContext.monthlyLimit}`,
512
+ }],
513
+ structuredContent: {
514
+ planName,
515
+ planId: billingContext.plan,
516
+ billingContext: billingContext.type,
517
+ messagesUsed: billingContext.messagesThisMonth,
518
+ messageLimit: billingContext.monthlyLimit === -1 ? null : billingContext.monthlyLimit,
519
+ credits: billingContext.credits,
520
+ periodEnd: new Date(new Date().getFullYear(), new Date().getMonth() + 1, 1).toISOString(),
521
+ },
522
+ };
523
+ },
524
+ };
525
+ ```
526
+
527
+ **Template (`packages/server/src/tools/templates/get-plan-usage.html`):**
528
+
529
+ ```html
530
+ <!DOCTYPE html>
531
+ <html>
532
+ <head>
533
+ <style>
534
+ * { margin: 0; padding: 0; box-sizing: border-box; }
535
+ body {
536
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
537
+ padding: 16px;
538
+ }
539
+ .card {
540
+ border-radius: 12px;
541
+ padding: 20px;
542
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
543
+ color: white;
544
+ max-width: 360px;
545
+ }
546
+ .card.dark {
547
+ background: linear-gradient(135deg, #434343 0%, #000000 100%);
548
+ }
549
+ .plan-name { font-size: 24px; font-weight: 700; margin-bottom: 4px; }
550
+ .usage-section {
551
+ background: rgba(255, 255, 255, 0.15);
552
+ border-radius: 8px;
553
+ padding: 12px;
554
+ margin: 16px 0 12px;
555
+ }
556
+ .progress-bar {
557
+ height: 8px;
558
+ background: rgba(255, 255, 255, 0.3);
559
+ border-radius: 4px;
560
+ overflow: hidden;
561
+ margin-top: 8px;
562
+ }
563
+ .progress-fill {
564
+ height: 100%;
565
+ background: white;
566
+ border-radius: 4px;
567
+ }
568
+ </style>
569
+ </head>
570
+ <body>
571
+ <div id="card" class="card">
572
+ <div id="plan-name" class="plan-name">Loading...</div>
573
+ <div class="usage-section">
574
+ <div id="usage-text">-</div>
575
+ <div class="progress-bar">
576
+ <div id="progress" class="progress-fill" style="width: 0%"></div>
577
+ </div>
578
+ </div>
579
+ </div>
580
+ <script>
581
+ (function() {
582
+ const data = window.openai?.toolOutput || {};
583
+ const theme = window.openai?.theme || 'light';
584
+
585
+ if (theme === 'dark') {
586
+ document.getElementById('card').classList.add('dark');
587
+ }
588
+
589
+ document.getElementById('plan-name').textContent = data.planName || 'Unknown';
590
+
591
+ const used = data.messagesUsed || 0;
592
+ const limit = data.messageLimit;
593
+
594
+ if (limit) {
595
+ document.getElementById('usage-text').textContent = `${used} / ${limit} messages`;
596
+ document.getElementById('progress').style.width = Math.min((used / limit) * 100, 100) + '%';
597
+ } else {
598
+ document.getElementById('usage-text').textContent = `${used} messages (unlimited)`;
599
+ }
600
+ })();
601
+ </script>
602
+ </body>
603
+ </html>
604
+ ```
605
+
606
+ ### Widget Best Practices
607
+
608
+ 1. **Always provide `content`**: The LLM needs text content to understand the result
609
+ 2. **Keep templates self-contained**: All styles and scripts should be inline
610
+ 3. **Support both themes**: Check `window.openai.theme` and apply appropriate styles
611
+ 4. **Handle missing data**: Your template should gracefully handle undefined values
612
+ 5. **Use semantic HTML**: Helps with accessibility and debugging
613
+ 6. **Minimize JavaScript**: Keep widget logic simple and fast
614
+ 7. **Test both themes**: Verify your widget looks good in light and dark mode
615
+
616
+ ## Best Practices
617
+
618
+ 1. **Validate inputs**: Always validate and sanitize user inputs before processing
619
+ 2. **Handle errors gracefully**: Return `isError: true` with a helpful message instead of throwing
620
+ 3. **Use timeouts**: Set reasonable timeouts for external requests
621
+ 4. **Respect rate limits**: If your tool calls external APIs, implement rate limiting
622
+ 5. **Keep descriptions clear**: The LLM uses the description to decide when to use the tool
623
+ 6. **Document parameters**: Provide clear descriptions for each input parameter
624
+
625
+ ## Exposing Native Tools via MCP Server
626
+
627
+ When you enable the MCP server export feature, your native tools can be accessed by external MCP clients like Claude Desktop or MCP Inspector. This allows other applications to use your app's tools.
628
+
629
+ ### Configuration
630
+
631
+ ```typescript
632
+ // config/app.config.ts
633
+ mcp: {
634
+ server: {
635
+ enabled: true,
636
+ exposeTools: 'native', // Expose native tools via MCP
637
+ oauth: {
638
+ enabled: true,
639
+ allowDynamicRegistration: true,
640
+ },
641
+ },
642
+ }
643
+ ```
644
+
645
+ ### Exposure Options
646
+
647
+ - **`'native'`**: Only expose native tools (recommended)
648
+ - **`'all'`**: Expose native tools plus tools from connected MCP servers
649
+ - **`string[]`**: Explicit list, e.g., `['web-scrape', 'get-plan-usage']`
650
+
651
+ ### How It Works
652
+
653
+ 1. External MCP client connects to `/mcp` endpoint
654
+ 2. Client authenticates via OAuth or API key
655
+ 3. Client calls `tools/list` to discover available tools
656
+ 4. Client calls `tools/call` to execute a tool
657
+ 5. Tool runs with the authenticated user's context
658
+
659
+ ### User Context in External Calls
660
+
661
+ When a tool is called via the MCP server, the `context` object includes the authenticated user's ID:
662
+
663
+ ```typescript
664
+ async execute(input, context) {
665
+ // context.userId is set from the OAuth token or API key
666
+ if (!context.userId) {
667
+ return {
668
+ content: [{ type: 'text', text: 'Authentication required' }],
669
+ isError: true,
670
+ };
671
+ }
672
+
673
+ // Tool has access to user's data
674
+ const userData = await db.user.findUnique({ where: { id: context.userId } });
675
+ // ...
676
+ }
677
+ ```
678
+
679
+ See [MCP Integration > MCP Server Export](./mcp.md#mcp-server-export) for full documentation.
680
+
681
+ ## Security Considerations
682
+
683
+ - Native tools run with server privileges - be careful about what capabilities you expose
684
+ - Validate URLs to prevent SSRF attacks (e.g., block internal IPs)
685
+ - Sanitize file paths if your tool accesses the filesystem
686
+ - Consider rate limiting to prevent abuse
687
+ - Use the `context.userId` to implement per-user restrictions if needed
688
+ - When exposing via MCP server, tools run with the authenticated user's permissions