@vertesia/tools-sdk 0.80.0-dev.20251121 → 0.80.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 (61) hide show
  1. package/README.md +122 -0
  2. package/lib/cjs/InteractionCollection.js +118 -0
  3. package/lib/cjs/InteractionCollection.js.map +1 -1
  4. package/lib/cjs/SkillCollection.js +318 -0
  5. package/lib/cjs/SkillCollection.js.map +1 -0
  6. package/lib/cjs/ToolCollection.js +98 -0
  7. package/lib/cjs/ToolCollection.js.map +1 -1
  8. package/lib/cjs/copy-assets.js +84 -0
  9. package/lib/cjs/copy-assets.js.map +1 -0
  10. package/lib/cjs/index.js +6 -1
  11. package/lib/cjs/index.js.map +1 -1
  12. package/lib/cjs/server.js +327 -0
  13. package/lib/cjs/server.js.map +1 -0
  14. package/lib/cjs/site/styles.js +621 -0
  15. package/lib/cjs/site/styles.js.map +1 -0
  16. package/lib/cjs/site/templates.js +932 -0
  17. package/lib/cjs/site/templates.js.map +1 -0
  18. package/lib/esm/InteractionCollection.js +83 -0
  19. package/lib/esm/InteractionCollection.js.map +1 -1
  20. package/lib/esm/SkillCollection.js +311 -0
  21. package/lib/esm/SkillCollection.js.map +1 -0
  22. package/lib/esm/ToolCollection.js +64 -0
  23. package/lib/esm/ToolCollection.js.map +1 -1
  24. package/lib/esm/copy-assets.js +81 -0
  25. package/lib/esm/copy-assets.js.map +1 -0
  26. package/lib/esm/index.js +4 -0
  27. package/lib/esm/index.js.map +1 -1
  28. package/lib/esm/server.js +323 -0
  29. package/lib/esm/server.js.map +1 -0
  30. package/lib/esm/site/styles.js +618 -0
  31. package/lib/esm/site/styles.js.map +1 -0
  32. package/lib/esm/site/templates.js +920 -0
  33. package/lib/esm/site/templates.js.map +1 -0
  34. package/lib/types/InteractionCollection.d.ts +29 -0
  35. package/lib/types/InteractionCollection.d.ts.map +1 -1
  36. package/lib/types/SkillCollection.d.ts +111 -0
  37. package/lib/types/SkillCollection.d.ts.map +1 -0
  38. package/lib/types/ToolCollection.d.ts +18 -0
  39. package/lib/types/ToolCollection.d.ts.map +1 -1
  40. package/lib/types/copy-assets.d.ts +14 -0
  41. package/lib/types/copy-assets.d.ts.map +1 -0
  42. package/lib/types/index.d.ts +4 -0
  43. package/lib/types/index.d.ts.map +1 -1
  44. package/lib/types/server.d.ts +72 -0
  45. package/lib/types/server.d.ts.map +1 -0
  46. package/lib/types/site/styles.d.ts +5 -0
  47. package/lib/types/site/styles.d.ts.map +1 -0
  48. package/lib/types/site/templates.d.ts +54 -0
  49. package/lib/types/site/templates.d.ts.map +1 -0
  50. package/lib/types/types.d.ts +152 -0
  51. package/lib/types/types.d.ts.map +1 -1
  52. package/package.json +18 -5
  53. package/src/InteractionCollection.ts +90 -0
  54. package/src/SkillCollection.ts +389 -0
  55. package/src/ToolCollection.ts +68 -0
  56. package/src/copy-assets.ts +104 -0
  57. package/src/index.ts +4 -0
  58. package/src/server.ts +444 -0
  59. package/src/site/styles.ts +617 -0
  60. package/src/site/templates.ts +956 -0
  61. package/src/types.ts +162 -0
package/src/server.ts ADDED
@@ -0,0 +1,444 @@
1
+ import { CatalogInteractionRef } from "@vertesia/common";
2
+ import { Context, Hono } from "hono";
3
+ import { cors } from "hono/cors";
4
+ import { HTTPException } from "hono/http-exception";
5
+ import { authorize } from "./auth.js";
6
+ import { InteractionCollection } from "./InteractionCollection.js";
7
+ import {
8
+ indexPage,
9
+ interactionCollectionPage,
10
+ skillCollectionPage,
11
+ toolCollectionPage
12
+ } from "./site/templates.js";
13
+ import { SkillCollection } from "./SkillCollection.js";
14
+ import { ToolCollection } from "./ToolCollection.js";
15
+ import type {
16
+ SkillDefinition,
17
+ ToolCollectionDefinition,
18
+ ToolDefinition
19
+ } from "./types.js";
20
+
21
+ /**
22
+ * MCP Provider interface for server configuration
23
+ */
24
+ export interface MCPProviderConfig {
25
+ name: string;
26
+ description?: string;
27
+ createMCPConnection: (session: any, config: Record<string, any>) => Promise<{
28
+ name: string;
29
+ url: string;
30
+ token: string;
31
+ }>;
32
+ }
33
+
34
+ /**
35
+ * Server configuration options
36
+ */
37
+ export interface ToolServerConfig {
38
+ /**
39
+ * Server title for HTML pages (default: 'Tools Server')
40
+ */
41
+ title?: string;
42
+ /**
43
+ * API prefix (default: '/api')
44
+ */
45
+ prefix?: string;
46
+ /**
47
+ * Tool collections to expose
48
+ */
49
+ tools?: ToolCollection[];
50
+ /**
51
+ * Interaction collections to expose
52
+ */
53
+ interactions?: InteractionCollection[];
54
+ /**
55
+ * Skill collections to expose
56
+ */
57
+ skills?: SkillCollection[];
58
+ /**
59
+ * MCP providers to expose
60
+ */
61
+ mcpProviders?: MCPProviderConfig[];
62
+ /**
63
+ * Disable HTML pages (default: false)
64
+ */
65
+ disableHtml?: boolean;
66
+ }
67
+
68
+ /**
69
+ * Create a Hono server for tools, interactions, and skills.
70
+ *
71
+ * @example
72
+ * ```typescript
73
+ * import { createToolServer, ToolCollection, SkillCollection } from "@vertesia/tools-sdk";
74
+ *
75
+ * const server = createToolServer({
76
+ * tools: [myToolCollection],
77
+ * skills: [mySkillCollection],
78
+ * });
79
+ *
80
+ * export default server;
81
+ * ```
82
+ */
83
+ export function createToolServer(config: ToolServerConfig): Hono {
84
+ const {
85
+ title = 'Tools Server',
86
+ prefix = '/api',
87
+ tools = [],
88
+ interactions = [],
89
+ skills = [],
90
+ mcpProviders = [],
91
+ disableHtml = false,
92
+ } = config;
93
+
94
+ const app = new Hono();
95
+
96
+ // Add CORS middleware globally
97
+ app.use('*', cors({ origin: '*', allowMethods: ['GET', 'POST', 'OPTIONS'] }));
98
+
99
+ // HTML pages (unless disabled)
100
+ if (!disableHtml) {
101
+ // Index page
102
+ app.get('/', (c) => {
103
+ return c.html(indexPage(tools, skills, interactions, mcpProviders, title));
104
+ });
105
+
106
+ // Tool collection pages
107
+ for (const coll of tools) {
108
+ app.get(`/tools/${coll.name}`, (c) => {
109
+ return c.html(toolCollectionPage(coll));
110
+ });
111
+ }
112
+
113
+ // Skill collection pages
114
+ for (const coll of skills) {
115
+ app.get(`/skills/${coll.name}`, (c) => {
116
+ return c.html(skillCollectionPage(coll));
117
+ });
118
+ }
119
+
120
+ // Interaction collection pages
121
+ for (const coll of interactions) {
122
+ app.get(`/interactions/${coll.name}`, (c) => {
123
+ return c.html(interactionCollectionPage(coll));
124
+ });
125
+ }
126
+ }
127
+
128
+ // Add base API route
129
+ app.get(prefix, (c) => {
130
+ // Skills are exposed as tools, so include them in the tools list
131
+ const allToolEndpoints = [
132
+ ...tools.map(col => `${prefix}/tools/${col.name}`),
133
+ ...skills.map(col => `${prefix}/skills/${col.name}`),
134
+ ];
135
+ return c.json({
136
+ message: 'Vertesia Tools API',
137
+ version: '1.0.0',
138
+ endpoints: {
139
+ tools: allToolEndpoints,
140
+ interactions: interactions.map(col => `${prefix}/interactions/${col.name}`),
141
+ mcp: mcpProviders.map(p => `${prefix}/mcp/${p.name}`),
142
+ }
143
+ });
144
+ });
145
+
146
+ // ================== Aggregate Endpoints ==================
147
+ // These must be registered BEFORE collection-specific routes
148
+
149
+ // GET /api/skills - Returns all skills from all collections
150
+ app.get(`${prefix}/skills`, (c) => {
151
+ const url = new URL(c.req.url);
152
+ const allSkills: ToolDefinition[] = [];
153
+
154
+ for (const coll of skills) {
155
+ allSkills.push(...coll.getToolDefinitions());
156
+ }
157
+
158
+ return c.json({
159
+ src: `${url.origin}${url.pathname}`,
160
+ title: 'All Skills',
161
+ description: 'All available skills across all collections',
162
+ tools: allSkills,
163
+ collections: skills.map(s => ({
164
+ name: s.name,
165
+ title: s.title,
166
+ description: s.description,
167
+ })),
168
+ } satisfies ToolCollectionDefinition & { collections: any[] });
169
+ });
170
+
171
+ // GET /api/tools - Returns all tools from all collections
172
+ app.get(`${prefix}/tools`, (c) => {
173
+ const url = new URL(c.req.url);
174
+ const allTools: ToolDefinition[] = [];
175
+
176
+ for (const coll of tools) {
177
+ allTools.push(...coll.getToolDefinitions());
178
+ }
179
+
180
+ return c.json({
181
+ src: `${url.origin}${url.pathname}`,
182
+ title: 'All Tools',
183
+ description: 'All available tools across all collections',
184
+ tools: allTools,
185
+ collections: tools.map(t => ({
186
+ name: t.name,
187
+ title: t.title,
188
+ description: t.description,
189
+ })),
190
+ } satisfies ToolCollectionDefinition & { collections: any[] });
191
+ });
192
+
193
+ // GET /api/interactions - Returns all interactions from all collections
194
+ app.get(`${prefix}/interactions`, (c) => {
195
+ const allInteractions: CatalogInteractionRef[] = [];
196
+
197
+ for (const coll of interactions) {
198
+ for (const inter of coll.interactions) {
199
+ allInteractions.push({
200
+ type: "app",
201
+ id: inter.name,
202
+ name: inter.name,
203
+ title: inter.title || inter.name,
204
+ description: inter.description,
205
+ tags: inter.tags || [],
206
+ });
207
+ }
208
+ }
209
+
210
+ return c.json({
211
+ title: 'All Interactions',
212
+ description: 'All available interactions across all collections',
213
+ interactions: allInteractions,
214
+ collections: interactions.map(i => ({
215
+ name: i.name,
216
+ title: i.title,
217
+ description: i.description,
218
+ })),
219
+ });
220
+ });
221
+
222
+ // Create tool collection endpoints
223
+ for (const coll of tools) {
224
+ app.route(`${prefix}/tools/${coll.name}`, createToolEndpoints(coll));
225
+ // Also expose at root for backwards compatibility
226
+ app.route(`${prefix}/${coll.name}`, createToolEndpoints(coll));
227
+ }
228
+
229
+ // Create interaction collection endpoints
230
+ for (const coll of interactions) {
231
+ app.route(`${prefix}/interactions/${coll.name}`, createInteractionEndpoints(coll));
232
+ }
233
+
234
+ // Create skill collection endpoints (exposed as tools)
235
+ for (const coll of skills) {
236
+ app.route(`${prefix}/skills/${coll.name}`, createSkillEndpoints(coll));
237
+ }
238
+
239
+ // Create MCP provider endpoints
240
+ if (mcpProviders.length > 0) {
241
+ app.route(`${prefix}/mcp`, createMCPEndpoints(mcpProviders));
242
+ }
243
+
244
+ // Global error handler
245
+ app.onError((err, c) => {
246
+ if (err instanceof HTTPException) {
247
+ return c.json({ error: err.message }, err.status);
248
+ }
249
+ console.error('Uncaught Error:', err);
250
+ return c.json({ error: 'Internal Server Error' }, 500);
251
+ });
252
+
253
+ return app;
254
+ }
255
+
256
+ // ================== Tool Endpoints ==================
257
+
258
+ function createToolEndpoints(coll: ToolCollection): Hono {
259
+ const endpoint = new Hono();
260
+
261
+ endpoint.post('/', (c: Context) => {
262
+ return coll.execute(c);
263
+ });
264
+
265
+ endpoint.get('/', (c) => {
266
+ const importSourceUrl = c.req.query('import') != null;
267
+ const url = new URL(c.req.url);
268
+ return c.json({
269
+ src: importSourceUrl
270
+ ? `${url.origin}/libs/vertesia-tools-${coll.name}.js`
271
+ : `${url.origin}${url.pathname}`,
272
+ title: coll.title || coll.name,
273
+ description: coll.description || '',
274
+ tools: coll.getToolDefinitions()
275
+ } satisfies ToolCollectionDefinition);
276
+ });
277
+
278
+ return endpoint;
279
+ }
280
+
281
+ // ================== Interaction Endpoints ==================
282
+
283
+ function createInteractionEndpoints(coll: InteractionCollection): Hono {
284
+ const endpoint = new Hono();
285
+
286
+ endpoint.get('/', (c: Context) => {
287
+ return c.json(coll.interactions.map(inter => ({
288
+ type: "app",
289
+ id: inter.name,
290
+ name: inter.name,
291
+ title: inter.title || inter.name,
292
+ description: inter.description,
293
+ tags: inter.tags || [],
294
+ } satisfies CatalogInteractionRef)));
295
+ });
296
+
297
+ endpoint.get('/:name', async (c: Context) => {
298
+ await authorize(c);
299
+ const name = c.req.param('name');
300
+ const inter = coll.getInteractionByName(name);
301
+ if (!inter) {
302
+ throw new HTTPException(404, {
303
+ message: "No interaction found with name: " + name
304
+ });
305
+ }
306
+ return c.json(inter);
307
+ });
308
+
309
+ return endpoint;
310
+ }
311
+
312
+ // ================== Skill Endpoints ==================
313
+
314
+ function createSkillEndpoints(coll: SkillCollection): Hono {
315
+ const endpoint = new Hono();
316
+
317
+ // List skills as tool definitions (tool collection format)
318
+ // This allows skills to be used exactly like tools
319
+ endpoint.get('/', (c: Context) => {
320
+ const url = new URL(c.req.url);
321
+ return c.json({
322
+ src: `${url.origin}${url.pathname}`,
323
+ title: coll.title || coll.name,
324
+ description: coll.description || '',
325
+ tools: coll.getToolDefinitions()
326
+ } satisfies ToolCollectionDefinition);
327
+ });
328
+
329
+ // Get scripts for a specific skill
330
+ // Returns all scripts bundled with the skill
331
+ endpoint.get('/:name/scripts', (c: Context) => {
332
+ const name = c.req.param('name');
333
+ const skillName = name.startsWith('skill_') ? name.slice(6) : name;
334
+ const skill = coll.getSkill(skillName);
335
+ if (!skill) {
336
+ throw new HTTPException(404, {
337
+ message: `Skill not found: ${skillName}`
338
+ });
339
+ }
340
+ return c.json({
341
+ skill_name: skill.name,
342
+ scripts: skill.scripts || []
343
+ });
344
+ });
345
+
346
+ // Get a specific script file
347
+ endpoint.get('/:name/scripts/:filename', (c: Context) => {
348
+ const name = c.req.param('name');
349
+ const filename = c.req.param('filename');
350
+ const skillName = name.startsWith('skill_') ? name.slice(6) : name;
351
+ const skill = coll.getSkill(skillName);
352
+ if (!skill) {
353
+ throw new HTTPException(404, {
354
+ message: `Skill not found: ${skillName}`
355
+ });
356
+ }
357
+ const script = skill.scripts?.find(s => s.name === filename);
358
+ if (!script) {
359
+ throw new HTTPException(404, {
360
+ message: `Script not found: ${filename}`
361
+ });
362
+ }
363
+ // Return as plain text with appropriate content type
364
+ const contentType = filename.endsWith('.py') ? 'text/x-python'
365
+ : filename.endsWith('.sh') ? 'text/x-shellscript'
366
+ : filename.endsWith('.js') ? 'text/javascript'
367
+ : 'text/plain';
368
+ return c.text(script.content, 200, { 'Content-Type': contentType });
369
+ });
370
+
371
+ // Get a specific skill by name
372
+ endpoint.get('/:name', (c: Context) => {
373
+ const name = c.req.param('name');
374
+ // Handle both "skill_name" and "name" formats
375
+ const skillName = name.startsWith('skill_') ? name.slice(6) : name;
376
+ const skill = coll.getSkill(skillName);
377
+ if (!skill) {
378
+ throw new HTTPException(404, {
379
+ message: `Skill not found: ${skillName}`
380
+ });
381
+ }
382
+ return c.json(skill satisfies SkillDefinition);
383
+ });
384
+
385
+ // Execute skill (standard tool execution format)
386
+ endpoint.post('/', (c: Context) => {
387
+ return coll.execute(c);
388
+ });
389
+
390
+ return endpoint;
391
+ }
392
+
393
+ // ================== MCP Endpoints ==================
394
+
395
+ function createMCPEndpoints(providers: MCPProviderConfig[]): Hono {
396
+ const endpoint = new Hono();
397
+
398
+ for (const p of providers) {
399
+ endpoint.post(`/${p.name}`, async (c: Context) => {
400
+ const session = await authorize(c);
401
+ const config = await readJsonBody(c);
402
+ const info = await p.createMCPConnection(session, config);
403
+ return c.json(info);
404
+ });
405
+
406
+ endpoint.get(`/${p.name}`, (c: Context) => c.json({
407
+ name: p.name,
408
+ description: p.description,
409
+ }));
410
+ }
411
+
412
+ return endpoint;
413
+ }
414
+
415
+ async function readJsonBody(ctx: Context): Promise<Record<string, any>> {
416
+ try {
417
+ const text = await ctx.req.text();
418
+ const jsonContent = text?.trim() || '';
419
+ if (!jsonContent) return {};
420
+ return JSON.parse(jsonContent) as Record<string, any>;
421
+ } catch (err: any) {
422
+ throw new HTTPException(400, {
423
+ message: "Failed to parse JSON body: " + err.message
424
+ });
425
+ }
426
+ }
427
+
428
+ // ================== Server Utilities ==================
429
+
430
+ /**
431
+ * Simple development server with static file handling
432
+ */
433
+ export function createDevServer(config: ToolServerConfig & {
434
+ staticHandler?: (c: Context, next: () => Promise<void>) => Promise<Response | void>;
435
+ }): Hono {
436
+ const app = createToolServer(config);
437
+
438
+ if (config.staticHandler) {
439
+ app.use('*', config.staticHandler);
440
+ }
441
+
442
+ return app;
443
+ }
444
+