@vertesia/tools-sdk 0.24.0-dev.202601221707

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 (144) hide show
  1. package/LICENSE +13 -0
  2. package/README.md +122 -0
  3. package/lib/cjs/InteractionCollection.js +164 -0
  4. package/lib/cjs/InteractionCollection.js.map +1 -0
  5. package/lib/cjs/SkillCollection.js +376 -0
  6. package/lib/cjs/SkillCollection.js.map +1 -0
  7. package/lib/cjs/ToolCollection.js +228 -0
  8. package/lib/cjs/ToolCollection.js.map +1 -0
  9. package/lib/cjs/ToolRegistry.js +111 -0
  10. package/lib/cjs/ToolRegistry.js.map +1 -0
  11. package/lib/cjs/auth.js +104 -0
  12. package/lib/cjs/auth.js.map +1 -0
  13. package/lib/cjs/build/validate.js +7 -0
  14. package/lib/cjs/build/validate.js.map +1 -0
  15. package/lib/cjs/copy-assets.js +84 -0
  16. package/lib/cjs/copy-assets.js.map +1 -0
  17. package/lib/cjs/index.js +31 -0
  18. package/lib/cjs/index.js.map +1 -0
  19. package/lib/cjs/package.json +3 -0
  20. package/lib/cjs/server/interactions.js +66 -0
  21. package/lib/cjs/server/interactions.js.map +1 -0
  22. package/lib/cjs/server/mcp.js +45 -0
  23. package/lib/cjs/server/mcp.js.map +1 -0
  24. package/lib/cjs/server/site.js +30 -0
  25. package/lib/cjs/server/site.js.map +1 -0
  26. package/lib/cjs/server/skills.js +114 -0
  27. package/lib/cjs/server/skills.js.map +1 -0
  28. package/lib/cjs/server/tools.js +104 -0
  29. package/lib/cjs/server/tools.js.map +1 -0
  30. package/lib/cjs/server/types.js +3 -0
  31. package/lib/cjs/server/types.js.map +1 -0
  32. package/lib/cjs/server/widgets.js +27 -0
  33. package/lib/cjs/server/widgets.js.map +1 -0
  34. package/lib/cjs/server.js +132 -0
  35. package/lib/cjs/server.js.map +1 -0
  36. package/lib/cjs/site/styles.js +621 -0
  37. package/lib/cjs/site/styles.js.map +1 -0
  38. package/lib/cjs/site/templates.js +968 -0
  39. package/lib/cjs/site/templates.js.map +1 -0
  40. package/lib/cjs/types.js +3 -0
  41. package/lib/cjs/types.js.map +1 -0
  42. package/lib/cjs/utils.js +31 -0
  43. package/lib/cjs/utils.js.map +1 -0
  44. package/lib/esm/InteractionCollection.js +125 -0
  45. package/lib/esm/InteractionCollection.js.map +1 -0
  46. package/lib/esm/SkillCollection.js +369 -0
  47. package/lib/esm/SkillCollection.js.map +1 -0
  48. package/lib/esm/ToolCollection.js +190 -0
  49. package/lib/esm/ToolCollection.js.map +1 -0
  50. package/lib/esm/ToolRegistry.js +106 -0
  51. package/lib/esm/ToolRegistry.js.map +1 -0
  52. package/lib/esm/auth.js +97 -0
  53. package/lib/esm/auth.js.map +1 -0
  54. package/lib/esm/build/validate.js +4 -0
  55. package/lib/esm/build/validate.js.map +1 -0
  56. package/lib/esm/copy-assets.js +81 -0
  57. package/lib/esm/copy-assets.js.map +1 -0
  58. package/lib/esm/index.js +11 -0
  59. package/lib/esm/index.js.map +1 -0
  60. package/lib/esm/server/interactions.js +63 -0
  61. package/lib/esm/server/interactions.js.map +1 -0
  62. package/lib/esm/server/mcp.js +42 -0
  63. package/lib/esm/server/mcp.js.map +1 -0
  64. package/lib/esm/server/site.js +27 -0
  65. package/lib/esm/server/site.js.map +1 -0
  66. package/lib/esm/server/skills.js +111 -0
  67. package/lib/esm/server/skills.js.map +1 -0
  68. package/lib/esm/server/tools.js +101 -0
  69. package/lib/esm/server/tools.js.map +1 -0
  70. package/lib/esm/server/types.js +2 -0
  71. package/lib/esm/server/types.js.map +1 -0
  72. package/lib/esm/server/widgets.js +24 -0
  73. package/lib/esm/server/widgets.js.map +1 -0
  74. package/lib/esm/server.js +128 -0
  75. package/lib/esm/server.js.map +1 -0
  76. package/lib/esm/site/styles.js +618 -0
  77. package/lib/esm/site/styles.js.map +1 -0
  78. package/lib/esm/site/templates.js +956 -0
  79. package/lib/esm/site/templates.js.map +1 -0
  80. package/lib/esm/types.js +2 -0
  81. package/lib/esm/types.js.map +1 -0
  82. package/lib/esm/utils.js +26 -0
  83. package/lib/esm/utils.js.map +1 -0
  84. package/lib/types/InteractionCollection.d.ts +48 -0
  85. package/lib/types/InteractionCollection.d.ts.map +1 -0
  86. package/lib/types/SkillCollection.d.ts +118 -0
  87. package/lib/types/SkillCollection.d.ts.map +1 -0
  88. package/lib/types/ToolCollection.d.ts +72 -0
  89. package/lib/types/ToolCollection.d.ts.map +1 -0
  90. package/lib/types/ToolRegistry.d.ts +41 -0
  91. package/lib/types/ToolRegistry.d.ts.map +1 -0
  92. package/lib/types/auth.d.ts +32 -0
  93. package/lib/types/auth.d.ts.map +1 -0
  94. package/lib/types/build/validate.d.ts +2 -0
  95. package/lib/types/build/validate.d.ts.map +1 -0
  96. package/lib/types/copy-assets.d.ts +14 -0
  97. package/lib/types/copy-assets.d.ts.map +1 -0
  98. package/lib/types/index.d.ts +11 -0
  99. package/lib/types/index.d.ts.map +1 -0
  100. package/lib/types/server/interactions.d.ts +4 -0
  101. package/lib/types/server/interactions.d.ts.map +1 -0
  102. package/lib/types/server/mcp.d.ts +4 -0
  103. package/lib/types/server/mcp.d.ts.map +1 -0
  104. package/lib/types/server/site.d.ts +4 -0
  105. package/lib/types/server/site.d.ts.map +1 -0
  106. package/lib/types/server/skills.d.ts +4 -0
  107. package/lib/types/server/skills.d.ts.map +1 -0
  108. package/lib/types/server/tools.d.ts +4 -0
  109. package/lib/types/server/tools.d.ts.map +1 -0
  110. package/lib/types/server/types.d.ts +62 -0
  111. package/lib/types/server/types.d.ts.map +1 -0
  112. package/lib/types/server/widgets.d.ts +9 -0
  113. package/lib/types/server/widgets.d.ts.map +1 -0
  114. package/lib/types/server.d.ts +27 -0
  115. package/lib/types/server.d.ts.map +1 -0
  116. package/lib/types/site/styles.d.ts +5 -0
  117. package/lib/types/site/styles.d.ts.map +1 -0
  118. package/lib/types/site/templates.d.ts +54 -0
  119. package/lib/types/site/templates.d.ts.map +1 -0
  120. package/lib/types/types.d.ts +280 -0
  121. package/lib/types/types.d.ts.map +1 -0
  122. package/lib/types/utils.d.ts +4 -0
  123. package/lib/types/utils.d.ts.map +1 -0
  124. package/package.json +58 -0
  125. package/src/InteractionCollection.ts +143 -0
  126. package/src/SkillCollection.ts +461 -0
  127. package/src/ToolCollection.ts +223 -0
  128. package/src/ToolRegistry.ts +135 -0
  129. package/src/auth.ts +123 -0
  130. package/src/build/validate.ts +3 -0
  131. package/src/copy-assets.ts +104 -0
  132. package/src/index.ts +12 -0
  133. package/src/server/interactions.ts +79 -0
  134. package/src/server/mcp.ts +51 -0
  135. package/src/server/site.ts +46 -0
  136. package/src/server/skills.ts +133 -0
  137. package/src/server/tools.ts +128 -0
  138. package/src/server/types.ts +65 -0
  139. package/src/server/widgets.ts +38 -0
  140. package/src/server.ts +160 -0
  141. package/src/site/styles.ts +617 -0
  142. package/src/site/templates.ts +994 -0
  143. package/src/types.ts +303 -0
  144. package/src/utils.ts +23 -0
@@ -0,0 +1,135 @@
1
+ import { HTTPException } from "hono/http-exception";
2
+ import { Tool, ToolDefinitionWithDefault, ToolExecutionContext, ToolExecutionPayload, ToolExecutionResult } from "./types.js";
3
+
4
+ /**
5
+ * Options for filtering tool definitions
6
+ */
7
+ export interface ToolFilterOptions {
8
+ /**
9
+ * If true, only return tools that are available by default (default !== false).
10
+ * If false or undefined, return all tools.
11
+ */
12
+ defaultOnly?: boolean;
13
+ /**
14
+ * List of tool names that are unlocked (available even if default: false).
15
+ * These tools will be included even when defaultOnly is true.
16
+ */
17
+ unlockedTools?: string[];
18
+ }
19
+
20
+ export class ToolRegistry {
21
+
22
+ registry: Record<string, Tool<any>> = {};
23
+
24
+ constructor(tools: Tool<any>[] = []) {
25
+ for (const tool of tools) {
26
+ this.registry[tool.name] = tool;
27
+ }
28
+ }
29
+
30
+ /**
31
+ * Get tool definitions with optional filtering.
32
+ * @param options - Filtering options
33
+ * @returns Filtered tool definitions
34
+ */
35
+ getDefinitions(options?: ToolFilterOptions): ToolDefinitionWithDefault[] {
36
+ const { defaultOnly, unlockedTools = [] } = options || {};
37
+ const unlockedSet = new Set(unlockedTools);
38
+
39
+ return Object.values(this.registry)
40
+ .filter(tool => {
41
+ // If not filtering by default, include all tools
42
+ if (!defaultOnly) return true;
43
+
44
+ // Include if tool is default (default !== false) or is in unlocked list
45
+ const isDefault = tool.default !== false;
46
+ const isUnlocked = unlockedSet.has(tool.name);
47
+ return isDefault || isUnlocked;
48
+ })
49
+ .map(tool => ({
50
+ name: tool.name,
51
+ description: tool.description,
52
+ input_schema: tool.input_schema,
53
+ default: tool.default,
54
+ }));
55
+ }
56
+
57
+ /**
58
+ * Get tools that are in reserve (default: false and not unlocked).
59
+ * @param unlockedTools - List of tool names that are unlocked
60
+ * @returns Tool definitions for reserve tools
61
+ */
62
+ getReserveTools(unlockedTools: string[] = []): ToolDefinitionWithDefault[] {
63
+ const unlockedSet = new Set(unlockedTools);
64
+
65
+ return Object.values(this.registry)
66
+ .filter(tool => tool.default === false && !unlockedSet.has(tool.name))
67
+ .map(tool => ({
68
+ name: tool.name,
69
+ description: tool.description,
70
+ input_schema: tool.input_schema,
71
+ default: tool.default,
72
+ }));
73
+ }
74
+
75
+ getTool<ParamsT extends Record<string, any>>(name: string): Tool<ParamsT> {
76
+ const tool = this.registry[name]
77
+ if (tool === undefined) {
78
+ throw new ToolNotFoundError(name);
79
+ }
80
+ return tool;
81
+ }
82
+
83
+ getTools() {
84
+ return Object.values(this.registry);
85
+ }
86
+
87
+ registerTool<ParamsT extends Record<string, any>>(tool: Tool<ParamsT>): void {
88
+ this.registry[tool.name] = tool;
89
+ }
90
+
91
+ runTool<ParamsT extends Record<string, any>>(payload: ToolExecutionPayload<ParamsT>, context: ToolExecutionContext): Promise<ToolExecutionResult> {
92
+ const toolName = payload.tool_use.tool_name;
93
+ const toolUseId = payload.tool_use.id;
94
+ const runId = payload.metadata?.run_id;
95
+ console.log(`[ToolRegistry] Executing tool: ${toolName}`, {
96
+ toolUseId,
97
+ runId,
98
+ input: sanitizeInput(payload.tool_use.tool_input),
99
+ });
100
+ return this.getTool(toolName).run(payload, context);
101
+ }
102
+
103
+ }
104
+
105
+
106
+ export class ToolNotFoundError extends HTTPException {
107
+ constructor(name: string) {
108
+ super(404, { message: "Tool function not found: " + name });
109
+ this.name = "ToolNotFoundError";
110
+ }
111
+ }
112
+
113
+ const SENSITIVE_KEYS = new Set([
114
+ 'apikey', 'api_key', 'token', 'secret', 'password', 'credential', 'credentials',
115
+ 'authorization', 'auth', 'key', 'private_key', 'access_token', 'refresh_token'
116
+ ]);
117
+
118
+ function sanitizeInput(input: Record<string, any> | null | undefined): Record<string, any> | null {
119
+ if (!input) return null;
120
+
121
+ const sanitized: Record<string, any> = {};
122
+ for (const [key, value] of Object.entries(input)) {
123
+ const lowerKey = key.toLowerCase();
124
+ if (SENSITIVE_KEYS.has(lowerKey) || lowerKey.includes('key') || lowerKey.includes('token') || lowerKey.includes('secret')) {
125
+ sanitized[key] = '[REDACTED]';
126
+ } else if (typeof value === 'string' && value.length > 50) {
127
+ sanitized[key] = value.slice(0, 50) + '...';
128
+ } else if (typeof value === 'object' && value !== null) {
129
+ sanitized[key] = Array.isArray(value) ? `[Array(${value.length})]` : '[Object]';
130
+ } else {
131
+ sanitized[key] = value;
132
+ }
133
+ }
134
+ return sanitized;
135
+ }
package/src/auth.ts ADDED
@@ -0,0 +1,123 @@
1
+ import { decodeEndpoints, VertesiaClient } from "@vertesia/client";
2
+ import { AuthTokenPayload } from "@vertesia/common";
3
+ import { Context } from "hono";
4
+ import { HTTPException } from "hono/http-exception";
5
+ import { createLocalJWKSet, decodeJwt, JSONWebKeySet, jwtVerify, JWTVerifyGetKey } from "jose";
6
+ import { ToolExecutionContext } from "./types.js";
7
+ const cache: Record<string, JWTVerifyGetKey> = {};
8
+
9
+ export async function getJwks(url: string) {
10
+ if (!cache.url) {
11
+ console.log('JWKS cache miss for: ', url);
12
+ const jwks = await fetch(url).then(r => {
13
+ if (r.ok) {
14
+ return r.json() as Promise<JSONWebKeySet>;
15
+ }
16
+ throw new Error("Fetching jwks failed with code: " + r.status);
17
+ }).catch(err => {
18
+ throw new Error("Failed to fetch jwks: " + err.message);
19
+ })
20
+ cache.url = createLocalJWKSet(jwks);
21
+ }
22
+ return cache.url;
23
+ }
24
+
25
+ export async function verifyToken(token: string) {
26
+ const decodedJwt = decodeJwt(token);
27
+ if (!decodedJwt.iss) {
28
+ throw new Error("No issuer URL found in JWT");
29
+ }
30
+ if (!isAllowedIssuer(decodedJwt.iss)) {
31
+ throw new Error("Issuer is not allowed: " + decodedJwt.iss);
32
+ }
33
+ const jwks = await getJwks(`${decodedJwt.iss}/.well-known/jwks`);
34
+ return await jwtVerify<AuthTokenPayload>(token, jwks);
35
+ }
36
+
37
+
38
+ export interface EndpointOverrides {
39
+ studio?: string;
40
+ store?: string;
41
+ token?: string;
42
+ }
43
+
44
+ export interface ToolContext {
45
+ toolName?: string;
46
+ toolUseId?: string;
47
+ runId?: string;
48
+ }
49
+
50
+ export async function authorize(ctx: Context, endpointOverrides?: EndpointOverrides, toolContext?: ToolContext) {
51
+ const auth = ctx.req.header('Authorization');
52
+ if (!auth) {
53
+ throw new HTTPException(401, {
54
+ message: `Missing Authorization header`
55
+ });
56
+ }
57
+ const [scheme, value] = auth.trim().split(' ');
58
+ if (scheme.toLowerCase() !== 'bearer') {
59
+ throw new HTTPException(401, {
60
+ message: `Authorization scheme ${scheme} is not supported`
61
+ });
62
+ }
63
+ if (!value) {
64
+ throw new HTTPException(401, {
65
+ message: `Missing bearer token value`
66
+ });
67
+ }
68
+ try {
69
+ const { payload } = await verifyToken(value);
70
+ const session = new AuthSession(value, payload, endpointOverrides, toolContext);
71
+ ctx.set("auth", session);
72
+ return session;
73
+ } catch (err: any) {
74
+ throw new HTTPException(401, {
75
+ message: err.message,
76
+ cause: err
77
+ });
78
+ }
79
+ }
80
+
81
+ export class AuthSession implements ToolExecutionContext {
82
+ _client: VertesiaClient | undefined;
83
+ endpoints: {
84
+ studio: string;
85
+ store: string;
86
+ token: string;
87
+ };
88
+ toolContext?: ToolContext;
89
+
90
+ constructor(
91
+ public token: string,
92
+ public payload: AuthTokenPayload,
93
+ endpointOverrides?: EndpointOverrides,
94
+ toolContext?: ToolContext
95
+ ) {
96
+ const decoded = decodeEndpoints(payload.endpoints);
97
+ // Use overrides from workflow config if provided, falling back to JWT endpoints
98
+ this.endpoints = {
99
+ studio: endpointOverrides?.studio || decoded.studio,
100
+ store: endpointOverrides?.store || decoded.store,
101
+ token: endpointOverrides?.token || decoded.token || payload.iss,
102
+ };
103
+ this.toolContext = toolContext;
104
+ }
105
+
106
+ async getClient() {
107
+ if (!this._client) {
108
+ const toolInfo = this.toolContext?.toolName ? ` for ${this.toolContext.toolName}` : '';
109
+ console.log(`[VertesiaClient] Initializing client${toolInfo}`, {
110
+ tool: this.toolContext?.toolName,
111
+ toolUseId: this.toolContext?.toolUseId,
112
+ runId: this.toolContext?.runId,
113
+ endpoints: this.endpoints,
114
+ });
115
+ this._client = await VertesiaClient.fromAuthToken(this.token, this.payload, this.endpoints);
116
+ }
117
+ return this._client;
118
+ }
119
+ }
120
+
121
+ function isAllowedIssuer(iss: string) {
122
+ return iss.endsWith(".vertesia.io") || iss.endsWith(".becomposable.com");
123
+ }
@@ -0,0 +1,3 @@
1
+ export function validate() {
2
+ //TODO
3
+ }
@@ -0,0 +1,104 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Copy runtime assets (skill files, prompt files, scripts) to dist folder
4
+ * These files are read from disk at runtime and need to be deployed with the app
5
+ *
6
+ * Usage:
7
+ * npx tools-sdk-copy-assets [srcDir] [distDir]
8
+ *
9
+ * Or import and call directly:
10
+ * import { copyRuntimeAssets } from '@vertesia/tools-sdk';
11
+ * copyRuntimeAssets('./src', './dist');
12
+ */
13
+ import { existsSync, readdirSync, statSync, mkdirSync, copyFileSync } from "fs";
14
+ import { dirname, join } from "path";
15
+
16
+ /**
17
+ * Recursively copy files matching a filter
18
+ */
19
+ function copyFilesRecursive(src: string, dest: string, fileFilter: (filename: string) => boolean): void {
20
+ if (!existsSync(src)) return;
21
+
22
+ const entries = readdirSync(src);
23
+
24
+ for (const entry of entries) {
25
+ const srcPath = join(src, entry);
26
+ const destPath = join(dest, entry);
27
+ const stat = statSync(srcPath);
28
+
29
+ if (stat.isDirectory()) {
30
+ // Recurse into directories
31
+ copyFilesRecursive(srcPath, destPath, fileFilter);
32
+ } else if (fileFilter(entry)) {
33
+ // Copy matching files
34
+ mkdirSync(dirname(destPath), { recursive: true });
35
+ copyFileSync(srcPath, destPath);
36
+ }
37
+ }
38
+ }
39
+
40
+ export interface CopyAssetsOptions {
41
+ /** Source directory (default: './src') */
42
+ srcDir?: string;
43
+ /** Destination directory (default: './dist') */
44
+ distDir?: string;
45
+ /** Whether to log progress (default: true) */
46
+ verbose?: boolean;
47
+ }
48
+
49
+ /**
50
+ * Copy runtime assets (skills, interactions) from src to dist
51
+ */
52
+ export function copyRuntimeAssets(options: CopyAssetsOptions = {}): void {
53
+ const {
54
+ srcDir = './src',
55
+ distDir = './dist',
56
+ verbose = true
57
+ } = options;
58
+
59
+ if (verbose) {
60
+ console.log('Copying runtime assets to dist...');
61
+ }
62
+
63
+ // Copy skill files (SKILL.md, SKILL.jst, *.py)
64
+ const skillsSrc = join(srcDir, 'skills');
65
+ const skillsDest = join(distDir, 'skills');
66
+
67
+ if (existsSync(skillsSrc)) {
68
+ copyFilesRecursive(skillsSrc, skillsDest, (filename) => {
69
+ return filename === 'SKILL.md' ||
70
+ filename === 'SKILL.jst' ||
71
+ filename.endsWith('.py');
72
+ });
73
+ if (verbose) {
74
+ console.log(' ✓ Skills assets (SKILL.md, SKILL.jst, *.py)');
75
+ }
76
+ }
77
+
78
+ // Copy interaction prompt files (prompt.jst, prompt.md)
79
+ const interactionsSrc = join(srcDir, 'interactions');
80
+ const interactionsDest = join(distDir, 'interactions');
81
+
82
+ if (existsSync(interactionsSrc)) {
83
+ copyFilesRecursive(interactionsSrc, interactionsDest, (filename) => {
84
+ return filename === 'prompt.jst' ||
85
+ filename === 'prompt.md';
86
+ });
87
+ if (verbose) {
88
+ console.log(' ✓ Interaction assets (prompt.jst, prompt.md)');
89
+ }
90
+ }
91
+
92
+ if (verbose) {
93
+ console.log('Runtime assets copied successfully!');
94
+ }
95
+ }
96
+
97
+ // CLI entry point
98
+ if (typeof process !== 'undefined' && process.argv[1]?.includes('copy-assets')) {
99
+ const args = process.argv.slice(2);
100
+ const srcDir = args[0] || './src';
101
+ const distDir = args[1] || './dist';
102
+
103
+ copyRuntimeAssets({ srcDir, distDir });
104
+ }
package/src/index.ts ADDED
@@ -0,0 +1,12 @@
1
+ export { authorize, AuthSession } from "./auth.js";
2
+ export { copyRuntimeAssets } from "./copy-assets.js";
3
+ export * from "./InteractionCollection.js";
4
+ export * from "./server.js";
5
+ export * from "./server/types.js";
6
+ export * from "./site/templates.js";
7
+ export * from "./SkillCollection.js";
8
+ export * from "./ToolCollection.js";
9
+ export * from "./ToolRegistry.js";
10
+ export * from "./types.js";
11
+
12
+
@@ -0,0 +1,79 @@
1
+ // ================== Interaction Endpoints ==================
2
+
3
+ import { CatalogInteractionRef } from "@vertesia/common";
4
+ import { Context, Hono } from "hono";
5
+ import { HTTPException } from "hono/http-exception";
6
+ import { authorize } from "../auth.js";
7
+ import { InteractionCollection } from "../InteractionCollection.js";
8
+ import { ToolServerConfig } from "./types.js";
9
+
10
+ export function createInteractionsRoute(app: Hono, basePath: string, config: ToolServerConfig) {
11
+ const { interactions = [] } = config;
12
+
13
+ // GET /api/interactions - Returns all interactions from all collections
14
+ app.get(basePath, (c) => {
15
+ const allInteractions: CatalogInteractionRef[] = [];
16
+
17
+ for (const coll of interactions) {
18
+ for (const inter of coll.interactions) {
19
+ allInteractions.push({
20
+ type: "app",
21
+ id: inter.name,
22
+ name: inter.name,
23
+ title: inter.title || inter.name,
24
+ description: inter.description,
25
+ tags: inter.tags || [],
26
+ });
27
+ }
28
+ }
29
+
30
+ return c.json({
31
+ title: 'All Interactions',
32
+ description: 'All available interactions across all collections',
33
+ interactions: allInteractions,
34
+ collections: interactions.map(i => ({
35
+ name: i.name,
36
+ title: i.title,
37
+ description: i.description,
38
+ })),
39
+ });
40
+ });
41
+
42
+ // Create interaction collection endpoints
43
+ for (const coll of interactions) {
44
+ app.route(`${basePath}/${coll.name}`, createInteractionEndpoints(coll));
45
+ }
46
+
47
+ }
48
+
49
+
50
+
51
+
52
+ function createInteractionEndpoints(coll: InteractionCollection): Hono {
53
+ const endpoint = new Hono();
54
+
55
+ endpoint.get('/', (c: Context) => {
56
+ return c.json(coll.interactions.map(inter => ({
57
+ type: "app",
58
+ id: inter.name,
59
+ name: inter.name,
60
+ title: inter.title || inter.name,
61
+ description: inter.description,
62
+ tags: inter.tags || [],
63
+ } satisfies CatalogInteractionRef)));
64
+ });
65
+
66
+ endpoint.get('/:name', async (c: Context) => {
67
+ await authorize(c);
68
+ const name = c.req.param('name');
69
+ const inter = coll.getInteractionByName(name);
70
+ if (!inter) {
71
+ throw new HTTPException(404, {
72
+ message: "No interaction found with name: " + name
73
+ });
74
+ }
75
+ return c.json(inter);
76
+ });
77
+
78
+ return endpoint;
79
+ }
@@ -0,0 +1,51 @@
1
+ // ================== MCP Endpoints ==================
2
+
3
+ import { Context, Hono } from "hono";
4
+ import { HTTPException } from "hono/http-exception";
5
+ import { authorize } from "../auth.js";
6
+ import { ToolServerConfig } from "../index.js";
7
+ import { MCPProviderConfig } from "./types.js";
8
+
9
+
10
+
11
+ export function createMcpRoute(app: Hono, basePath: string, config: ToolServerConfig) {
12
+ const { mcpProviders = [] } = config;
13
+ // Create MCP provider endpoints
14
+ if (mcpProviders.length > 0) {
15
+ app.route(basePath, createMCPEndpoints(mcpProviders));
16
+ }
17
+
18
+ }
19
+
20
+ function createMCPEndpoints(providers: MCPProviderConfig[]): Hono {
21
+ const endpoint = new Hono();
22
+
23
+ for (const p of providers) {
24
+ endpoint.post(`/${p.name}`, async (c: Context) => {
25
+ const session = await authorize(c);
26
+ const config = await readJsonBody(c);
27
+ const info = await p.createMCPConnection(session, config);
28
+ return c.json(info);
29
+ });
30
+
31
+ endpoint.get(`/${p.name}`, (c: Context) => c.json({
32
+ name: p.name,
33
+ description: p.description,
34
+ }));
35
+ }
36
+
37
+ return endpoint;
38
+ }
39
+
40
+ async function readJsonBody(ctx: Context): Promise<Record<string, any>> {
41
+ try {
42
+ const text = await ctx.req.text();
43
+ const jsonContent = text?.trim() || '';
44
+ if (!jsonContent) return {};
45
+ return JSON.parse(jsonContent) as Record<string, any>;
46
+ } catch (err: any) {
47
+ throw new HTTPException(400, {
48
+ message: "Failed to parse JSON body: " + err.message
49
+ });
50
+ }
51
+ }
@@ -0,0 +1,46 @@
1
+ import { Hono } from "hono";
2
+ import {
3
+ indexPage,
4
+ interactionCollectionPage,
5
+ skillCollectionPage,
6
+ toolCollectionPage
7
+ } from "../site/templates.js";
8
+ import { ToolServerConfig } from "./types.js";
9
+
10
+
11
+ export function createSiteRoute(app: Hono, basePath: string, config: ToolServerConfig) {
12
+ const {
13
+ title = 'Tools Server',
14
+ tools = [],
15
+ interactions = [],
16
+ skills = [],
17
+ mcpProviders = [],
18
+ } = config;
19
+
20
+ // Index page
21
+ app.get(`${basePath}/`, (c) => {
22
+ return c.html(indexPage(tools, skills, interactions, mcpProviders, title));
23
+ });
24
+
25
+ // Tool collection pages
26
+ for (const coll of tools) {
27
+ app.get(`${basePath}/tools/${coll.name}`, (c) => {
28
+ return c.html(toolCollectionPage(coll));
29
+ });
30
+ }
31
+
32
+ // Skill collection pages
33
+ for (const coll of skills) {
34
+ app.get(`${basePath}/skills/${coll.name}`, (c) => {
35
+ return c.html(skillCollectionPage(coll));
36
+ });
37
+ }
38
+
39
+ // Interaction collection pages
40
+ for (const coll of interactions) {
41
+ app.get(`${basePath}/interactions/${coll.name}`, (c) => {
42
+ return c.html(interactionCollectionPage(coll));
43
+ });
44
+ }
45
+
46
+ }
@@ -0,0 +1,133 @@
1
+ // ================== Skill Endpoints ==================
2
+
3
+ import { Context, Hono } from "hono";
4
+ import { HTTPException } from "hono/http-exception";
5
+ import { SkillCollection } from "../SkillCollection.js";
6
+ import { SkillDefinition, ToolCollectionDefinition, ToolDefinition } from "../types.js";
7
+ import { makeScriptUrl } from "../utils.js";
8
+ import { ToolContext, ToolServerConfig } from "./types.js";
9
+
10
+ export function createSkillsRoute(app: Hono, basePath: string, config: ToolServerConfig) {
11
+ const { skills = [] } = config;
12
+
13
+ // Build a map of skill name -> collection for routing
14
+ const skillToCollection = new Map<string, SkillCollection>();
15
+ for (const coll of skills) {
16
+ for (const skill of coll.getSkillDefinitions()) {
17
+ skillToCollection.set(skill.name, coll);
18
+ // Also map the learn_ prefixed name
19
+ skillToCollection.set(`learn_${skill.name}`, coll);
20
+ }
21
+ }
22
+
23
+ // GET /api/skills - Returns all skills from all collections
24
+ app.get(basePath, (c) => {
25
+ const url = new URL(c.req.url);
26
+ const allSkills: ToolDefinition[] = [];
27
+
28
+ for (const coll of skills) {
29
+ allSkills.push(...coll.getToolDefinitions());
30
+ }
31
+
32
+ return c.json({
33
+ src: `${url.origin}${url.pathname}`,
34
+ title: 'All Skills',
35
+ description: 'All available skills across all collections',
36
+ tools: allSkills,
37
+ collections: skills.map(s => ({
38
+ name: s.name,
39
+ title: s.title,
40
+ description: s.description,
41
+ })),
42
+ } satisfies ToolCollectionDefinition & { collections: any[] });
43
+ });
44
+
45
+ // POST /api/skills - Route to the correct collection based on tool_name
46
+ app.post(basePath, async (c) => {
47
+ const ctx = c as unknown as ToolContext;
48
+
49
+ // Payload is already parsed and validated by middleware
50
+ if (!ctx.payload) {
51
+ throw new HTTPException(400, {
52
+ message: 'Invalid or missing skill execution payload. Expected { tool_use: { id, tool_name, tool_input? }, metadata? }'
53
+ });
54
+ }
55
+
56
+ const toolName = ctx.payload.tool_use.tool_name;
57
+
58
+ // Find the collection for this skill
59
+ const collection = skillToCollection.get(toolName);
60
+ if (!collection) {
61
+ // Extract skill name for better error message
62
+ const skillName = toolName.startsWith('learn_') ? toolName.slice(6) : toolName;
63
+ throw new HTTPException(404, {
64
+ message: `Skill not found: ${skillName}. Available skills: ${Array.from(skillToCollection.keys()).filter(k => !k.startsWith('learn_')).join(', ')}`
65
+ });
66
+ }
67
+
68
+ // Delegate to the collection's execute method with pre-parsed payload
69
+ return collection.execute(c, ctx.payload);
70
+ });
71
+
72
+ // Create skill collection endpoints (exposed as tools)
73
+ for (const coll of skills) {
74
+ app.route(`${basePath}/${coll.name}`, createSkillEndpoints(coll));
75
+ }
76
+
77
+ }
78
+
79
+ function createSkillEndpoints(coll: SkillCollection): Hono {
80
+ const endpoint = new Hono();
81
+
82
+ // List skills as tool definitions (tool collection format)
83
+ // This allows skills to be used exactly like tools
84
+ endpoint.get('/', (c: Context) => {
85
+ const url = new URL(c.req.url);
86
+ return c.json({
87
+ src: `${url.origin}${url.pathname}`,
88
+ title: coll.title || coll.name,
89
+ description: coll.description || '',
90
+ tools: coll.getToolDefinitions()
91
+ } satisfies ToolCollectionDefinition);
92
+ });
93
+
94
+ // Get scripts for a specific skill
95
+ // Returns all scripts bundled with the skill
96
+ endpoint.get('/:name/scripts', (c: Context) => {
97
+ const name = c.req.param('name');
98
+ const skillName = name.startsWith('learn_') ? name.slice(6) : name;
99
+ const skill = coll.getSkill(skillName);
100
+ if (!skill) {
101
+ throw new HTTPException(404, {
102
+ message: `Skill not found: ${skillName}`
103
+ });
104
+ }
105
+ const url = new URL(c.req.url);
106
+ return c.json({
107
+ skill_name: skill.name,
108
+ scripts: skill.scripts ? skill.scripts.map(s => makeScriptUrl(url.origin, s)) : []
109
+ });
110
+ });
111
+
112
+
113
+ // Get a specific skill by name
114
+ endpoint.get('/:name', (c: Context) => {
115
+ const name = c.req.param('name');
116
+ // Handle both "learn_name" and "name" formats
117
+ const skillName = name.startsWith('learn_') ? name.slice(6) : name;
118
+ const skill = coll.getSkill(skillName);
119
+ if (!skill) {
120
+ throw new HTTPException(404, {
121
+ message: `Skill not found: ${skillName}`
122
+ });
123
+ }
124
+ return c.json(skill satisfies SkillDefinition);
125
+ });
126
+
127
+ // Execute skill (standard tool execution format)
128
+ endpoint.post('/', (c: Context) => {
129
+ return coll.execute(c);
130
+ });
131
+
132
+ return endpoint;
133
+ }