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.
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +25 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands/add-infra.d.ts +6 -0
- package/dist/commands/add-infra.d.ts.map +1 -0
- package/dist/commands/add-infra.js +160 -0
- package/dist/commands/add-infra.js.map +1 -0
- package/dist/commands/build.d.ts +2 -0
- package/dist/commands/build.d.ts.map +1 -0
- package/dist/commands/build.js +63 -0
- package/dist/commands/build.js.map +1 -0
- package/dist/commands/db-sync.d.ts +13 -0
- package/dist/commands/db-sync.d.ts.map +1 -0
- package/dist/commands/db-sync.js +108 -0
- package/dist/commands/db-sync.js.map +1 -0
- package/dist/commands/dev.d.ts +7 -0
- package/dist/commands/dev.d.ts.map +1 -0
- package/dist/commands/dev.js +61 -0
- package/dist/commands/dev.js.map +1 -0
- package/dist/commands/init.d.ts +9 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +214 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +57 -0
- package/dist/index.js.map +1 -0
- package/dist/templates/.env.example +24 -0
- package/dist/templates/README.md +81 -0
- package/dist/templates/app/components/AcceptInviteClient.tsx +10 -0
- package/dist/templates/app/components/AdminDashboardClient.tsx +10 -0
- package/dist/templates/app/components/AdminTeamClient.tsx +10 -0
- package/dist/templates/app/components/AdminTeamsClient.tsx +10 -0
- package/dist/templates/app/components/AdminUsersClient.tsx +10 -0
- package/dist/templates/app/components/ApiKeysClient.tsx +10 -0
- package/dist/templates/app/components/AutomationsClient.tsx +10 -0
- package/dist/templates/app/components/ChatClient.tsx +13 -0
- package/dist/templates/app/components/ClientOnly.tsx +6 -0
- package/dist/templates/app/components/DocumentsClient.tsx +10 -0
- package/dist/templates/app/components/OAuthConsentClient.tsx +10 -0
- package/dist/templates/app/components/PricingClient.tsx +10 -0
- package/dist/templates/app/components/TeamSettingsClient.tsx +10 -0
- package/dist/templates/app/components/VerifyEmailClient.tsx +10 -0
- package/dist/templates/app/entry.client.tsx +12 -0
- package/dist/templates/app/entry.server.tsx +67 -0
- package/dist/templates/app/root.tsx +91 -0
- package/dist/templates/app/routes/_index.tsx +82 -0
- package/dist/templates/app/routes/admin._index.tsx +57 -0
- package/dist/templates/app/routes/admin.teams.$teamId.tsx +57 -0
- package/dist/templates/app/routes/admin.teams._index.tsx +57 -0
- package/dist/templates/app/routes/admin.users.tsx +57 -0
- package/dist/templates/app/routes/api-keys.tsx +57 -0
- package/dist/templates/app/routes/automations.tsx +57 -0
- package/dist/templates/app/routes/chat._index.tsx +11 -0
- package/dist/templates/app/routes/chat.admin._index.tsx +10 -0
- package/dist/templates/app/routes/chat.admin.teams.$teamId.tsx +10 -0
- package/dist/templates/app/routes/chat.admin.teams._index.tsx +10 -0
- package/dist/templates/app/routes/chat.admin.users.tsx +10 -0
- package/dist/templates/app/routes/chat.api-keys.tsx +10 -0
- package/dist/templates/app/routes/chat.automations.tsx +10 -0
- package/dist/templates/app/routes/chat.documents.tsx +10 -0
- package/dist/templates/app/routes/chat.team.$teamId.settings.tsx +10 -0
- package/dist/templates/app/routes/chat.thread.$threadId.tsx +11 -0
- package/dist/templates/app/routes/chat.tsx +39 -0
- package/dist/templates/app/routes/documents.tsx +57 -0
- package/dist/templates/app/routes/invite.$token.tsx +10 -0
- package/dist/templates/app/routes/login.tsx +334 -0
- package/dist/templates/app/routes/oauth.consent.tsx +10 -0
- package/dist/templates/app/routes/pricing.tsx +10 -0
- package/dist/templates/app/routes/privacy.tsx +197 -0
- package/dist/templates/app/routes/register.tsx +398 -0
- package/dist/templates/app/routes/shared.$shareId.tsx +226 -0
- package/dist/templates/app/routes/team.$teamId.settings.tsx +57 -0
- package/dist/templates/app/routes/terms.tsx +173 -0
- package/dist/templates/app/routes/thread.$threadId.tsx +102 -0
- package/dist/templates/app/routes/verify-email.tsx +10 -0
- package/dist/templates/app/routes.ts +47 -0
- package/dist/templates/config/app.config.ts +216 -0
- package/dist/templates/docs/admin.md +257 -0
- package/dist/templates/docs/api-keys.md +403 -0
- package/dist/templates/docs/authentication.md +247 -0
- package/dist/templates/docs/configuration.md +1212 -0
- package/dist/templates/docs/custom-pages.md +466 -0
- package/dist/templates/docs/deployment.md +362 -0
- package/dist/templates/docs/development.md +411 -0
- package/dist/templates/docs/documents.md +293 -0
- package/dist/templates/docs/extensions.md +639 -0
- package/dist/templates/docs/index.md +139 -0
- package/dist/templates/docs/installation.md +286 -0
- package/dist/templates/docs/mcp.md +952 -0
- package/dist/templates/docs/native-tools.md +688 -0
- package/dist/templates/docs/queue.md +514 -0
- package/dist/templates/docs/scheduled-prompts.md +279 -0
- package/dist/templates/docs/settings.md +415 -0
- package/dist/templates/docs/slack.md +318 -0
- package/dist/templates/docs/styling.md +288 -0
- package/dist/templates/extensions/agents/.gitkeep +0 -0
- package/dist/templates/extensions/pages/.gitkeep +0 -0
- package/dist/templates/extensions/payment-plans/.gitkeep +0 -0
- package/dist/templates/index.html +16 -0
- package/dist/templates/infra-aws/.github/workflows/deploy.yml +95 -0
- package/dist/templates/infra-aws/README.md +207 -0
- package/dist/templates/infra-aws/bin/cdk.ts +18 -0
- package/dist/templates/infra-aws/cdk.json +43 -0
- package/dist/templates/infra-aws/config/deployment.ts +156 -0
- package/dist/templates/infra-aws/lib/chaaskit-stack.ts +419 -0
- package/dist/templates/infra-aws/package.json +27 -0
- package/dist/templates/infra-aws/scripts/build-app.sh +63 -0
- package/dist/templates/infra-aws/tsconfig.json +25 -0
- package/dist/templates/package.json +46 -0
- package/dist/templates/prisma/schema/base.prisma +584 -0
- package/dist/templates/prisma/schema/custom.prisma +24 -0
- package/dist/templates/prisma/schema.prisma +271 -0
- package/dist/templates/public/favicon.svg +4 -0
- package/dist/templates/public/logo.svg +4 -0
- package/dist/templates/react-router.config.ts +11 -0
- package/dist/templates/server.js +52 -0
- package/dist/templates/src/main.tsx +8 -0
- package/dist/templates/tsconfig.json +26 -0
- package/dist/templates/vite.config.ts +26 -0
- 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
|