diff --git a/templates/vercel-executor/README.md b/templates/vercel-executor/README.md index 0a8133f..078a35f 100644 --- a/templates/vercel-executor/README.md +++ b/templates/vercel-executor/README.md @@ -1,62 +1,77 @@ -# TPMJS Executor Template +# TPMJS Executor for Vercel -Deploy your own executor for running TPMJS tools on Vercel. +Deploy your own TPMJS tool executor using the **Deno runtime** on Vercel for secure, isolated code execution. + +## Features + +- **Native HTTP Imports**: Deno natively supports importing from esm.sh +- **Full Control**: Your infrastructure, your environment variables +- **Privacy**: No data passes through TPMJS servers +- **One-Click Deploy**: Deploy to Vercel in minutes ## One-Click Deploy [![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https://github.com/tpmjs/tpmjs/tree/main/templates/vercel-executor&project-name=tpmjs-executor&repository-name=tpmjs-executor) -## What is an Executor? +## How It Works -An executor is a service that runs TPMJS tools. By default, TPMJS uses a shared executor, but you can deploy your own for: +This executor uses the [Vercel Deno Runtime](https://github.com/vercel-community/deno) to: -- **Full control**: Run tools on your own infrastructure -- **Custom environment**: Inject your own environment variables and secrets -- **Privacy**: Keep tool execution data on your own servers -- **Performance**: Deploy in regions closest to your users +1. Receive tool execution requests via POST `/api/execute-tool` +2. Dynamically import the npm package from esm.sh (Deno natively supports HTTP imports!) +3. Execute the tool with your parameters +4. Return the result + +This provides the same execution model as the Railway executor, but deployed to your own Vercel account. ## API Endpoints -### POST /api/execute-tool - -Execute a TPMJS tool. - -**Request:** -```json -{ - "packageName": "@tpmjs/hello", - "name": "helloWorld", - "version": "latest", - "params": { "name": "World" }, - "env": { "MY_SECRET": "value" } -} -``` - -**Response:** -```json -{ - "success": true, - "output": "Hello, World!", - "executionTimeMs": 123 -} -``` - ### GET /api/health Check executor health status. +```bash +curl https://your-executor.vercel.app/api/health +``` + **Response:** ```json { "status": "ok", "version": "1.0.0", "info": { - "runtime": "vercel-serverless", + "runtime": "deno", + "httpImports": true, "timestamp": "2024-01-01T00:00:00.000Z" } } ``` +### POST /api/execute-tool + +Execute a TPMJS tool. + +```bash +curl -X POST https://your-executor.vercel.app/api/execute-tool \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer your-api-key" \ + -d '{ + "packageName": "@anthropic-ai/tpmjs-hello", + "name": "helloWorld", + "version": "latest", + "params": { "name": "World" } + }' +``` + +**Response:** +```json +{ + "success": true, + "output": "Hello, World!", + "executionTimeMs": 234 +} +``` + ## Configuration ### Environment Variables @@ -65,46 +80,64 @@ Check executor health status. |----------|----------|-------------| | `EXECUTOR_API_KEY` | No | API key for authentication. If set, requests must include `Authorization: Bearer ` header. | +Add custom environment variables for your tools (e.g., `MY_API_KEY`, `DATABASE_URL`) in your Vercel project settings. + ### Setting Up API Key Authentication 1. Go to your Vercel project settings 2. Add an environment variable: `EXECUTOR_API_KEY` with a secure random value 3. When configuring your executor in TPMJS, enter this key in the "API Key" field -## How It Works +## Connecting to TPMJS -1. TPMJS sends a request to your executor with package name, tool name, and parameters -2. The executor dynamically imports the package from [esm.sh](https://esm.sh) -3. The tool's `execute()` function is called with the provided parameters -4. The result is returned to TPMJS +1. Go to your TPMJS collection or agent settings +2. In "Executor Configuration", select "Custom Executor" +3. Enter your executor URL: `https://your-executor.vercel.app` +4. Enter your API key (if configured) +5. Click "Verify Connection" to test ## Local Development ```bash -# Install dependencies -npm install +# Install Vercel CLI +npm install -g vercel + +# Login to Vercel +vercel login # Run development server -npm run dev +vercel dev # Test the health endpoint curl http://localhost:3000/api/health - -# Test tool execution -curl -X POST http://localhost:3000/api/execute-tool \ - -H "Content-Type: application/json" \ - -d '{"packageName":"@anthropic-ai/tpmjs-hello","name":"helloWorld","params":{"name":"Test"}}' ``` -## Security Considerations +**Note:** Local development requires the Vercel CLI. Run `vercel login` first. -- Always set `EXECUTOR_API_KEY` in production to prevent unauthorized access -- The executor runs tools in a serverless environment with limited capabilities -- Environment variables injected via `env` are available only during execution -- Tools are imported from esm.sh, a trusted CDN for npm packages +## Security + +- Set `EXECUTOR_API_KEY` to require authentication for all requests +- Tools are loaded dynamically from esm.sh +- Each request gets fresh environment variable injection +- CORS headers allow cross-origin requests (configurable) + +## How It Compares to TPMJS Default Executor + +| Feature | TPMJS Default (Railway) | Your Vercel Executor | +|---------|------------------------|---------------------| +| Runtime | Deno on Railway | Deno on Vercel | +| Cost | Free (TPMJS hosted) | Your Vercel usage | +| Env Vars | Stored in TPMJS | Stored in your Vercel project | +| Privacy | Requests go through TPMJS | Direct to your executor | +| Control | Managed by TPMJS | Fully yours | + +## Pricing + +Vercel's free tier includes generous limits for serverless functions. See [Vercel Pricing](https://vercel.com/pricing) for details. ## Support -- [TPMJS Documentation](https://tpmjs.com/docs) -- [Executor Documentation](https://tpmjs.com/docs/executors) +- [TPMJS Custom Executors Documentation](https://tpmjs.com/docs/executors) +- [TPMJS Custom Executor Tutorial](https://tpmjs.com/docs/tutorials/custom-executor) +- [Vercel Deno Runtime](https://github.com/vercel-community/deno) - [GitHub Issues](https://github.com/tpmjs/tpmjs/issues) diff --git a/templates/vercel-executor/api/execute-tool.ts b/templates/vercel-executor/api/execute-tool.ts new file mode 100644 index 0000000..e0d65d4 --- /dev/null +++ b/templates/vercel-executor/api/execute-tool.ts @@ -0,0 +1,256 @@ +/** + * Tool Execution Endpoint (Deno Runtime) + * + * POST /api/execute-tool + * Execute a TPMJS tool with parameters + * + * Uses Deno's native HTTP import support to load tools from esm.sh + */ + +// @ts-ignore - Deno global +declare const Deno: { + env: { + get(key: string): string | undefined; + set(key: string, value: string): void; + }; +}; + +interface ExecuteToolRequest { + packageName: string; + name: string; + version?: string; + importUrl?: string; + params: Record; + env?: Record; +} + +interface ExecuteToolResponse { + success: boolean; + output?: unknown; + error?: string; + executionTimeMs: number; +} + +// Simple in-memory cache for tool modules +// biome-ignore lint/suspicious/noExplicitAny: Tool types are dynamic +const moduleCache = new Map(); +const CACHE_TTL_MS = 2 * 60 * 1000; // 2 minutes + +export default async function handler(req: Request): Promise { + // Handle CORS preflight + if (req.method === 'OPTIONS') { + return new Response(null, { + status: 200, + headers: { + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Methods': 'POST, OPTIONS', + 'Access-Control-Allow-Headers': 'Content-Type, Authorization', + }, + }); + } + + // Check authorization if EXECUTOR_API_KEY is set + const apiKey = Deno.env.get('EXECUTOR_API_KEY'); + if (apiKey) { + const authHeader = req.headers.get('Authorization'); + if (!authHeader || authHeader !== `Bearer ${apiKey}`) { + return Response.json( + { success: false, error: 'Unauthorized' }, + { + status: 401, + headers: { 'Access-Control-Allow-Origin': '*' }, + } + ); + } + } + + const startTime = Date.now(); + let packageName = 'unknown'; + let toolName = 'unknown'; + + try { + const body: ExecuteToolRequest = await req.json(); + packageName = body.packageName || 'unknown'; + toolName = body.name || 'unknown'; + + const { version, importUrl, params, env } = body; + + if (!body.packageName || !body.name) { + return Response.json( + { + success: false, + error: 'Missing required fields: packageName, name', + executionTimeMs: Date.now() - startTime, + }, + { + status: 400, + headers: { 'Access-Control-Allow-Origin': '*' }, + } + ); + } + + const resolvedVersion = version || 'latest'; + const cacheKey = `${packageName}@${resolvedVersion}::${toolName}`; + + // Inject environment variables before loading/executing tool + if (env && typeof env === 'object') { + for (const [key, value] of Object.entries(env)) { + Deno.env.set(key, String(value)); + } + } + + // biome-ignore lint/suspicious/noExplicitAny: Tool types are dynamic + let toolModule: any; + const cachedEntry = moduleCache.get(cacheKey); + const now = Date.now(); + + if (cachedEntry && now < cachedEntry.expiresAt) { + toolModule = cachedEntry.module; + } else { + // Dynamic import from esm.sh - Deno supports this natively! + const url = importUrl || `https://esm.sh/${packageName}@${resolvedVersion}`; + const module = await import(url); + + let rawExport = module[toolName]; + + if (!rawExport) { + return Response.json( + { + success: false, + error: `Export "${toolName}" not found in module`, + availableExports: Object.keys(module), + executionTimeMs: Date.now() - startTime, + }, + { + status: 404, + headers: { 'Access-Control-Allow-Origin': '*' }, + } + ); + } + + // Handle factory functions (tools that need to be called to create the tool instance) + if (typeof rawExport === 'function' && !rawExport.execute) { + let factoryResult = null; + + // Strategy 1: Try calling with no arguments + try { + factoryResult = rawExport(); + if (factoryResult?.execute) { + rawExport = factoryResult; + } + } catch { + // Try other strategies + } + + // Strategy 2: Try with env config object + if (!factoryResult?.execute && env) { + const configVariations = [ + { ...env }, + // Extract API key if present + (() => { + const apiKeyEntry = Object.entries(env).find(([key]) => + key.toUpperCase().includes('API_KEY') + ); + return apiKeyEntry ? { apiKey: apiKeyEntry[1] } : null; + })(), + ].filter(Boolean); + + for (const config of configVariations) { + try { + factoryResult = rawExport(config); + if (factoryResult?.execute) { + rawExport = factoryResult; + break; + } + } catch { + // Try next config + } + } + } + + // Strategy 3: Try with first env value (single-arg pattern) + if (!factoryResult?.execute && env) { + const firstValue = Object.values(env)[0]; + if (firstValue) { + try { + factoryResult = rawExport(firstValue); + if (factoryResult?.execute) { + rawExport = factoryResult; + } + } catch { + // Factory failed + } + } + } + + if (!rawExport?.execute) { + return Response.json( + { + success: false, + error: `Tool "${toolName}" is a factory function but couldn't be initialized`, + hint: 'This tool may require specific configuration. Check package documentation.', + executionTimeMs: Date.now() - startTime, + }, + { + status: 400, + headers: { 'Access-Control-Allow-Origin': '*' }, + } + ); + } + } + + toolModule = rawExport; + + if (!toolModule.execute) { + return Response.json( + { + success: false, + error: 'Tool missing execute function', + executionTimeMs: Date.now() - startTime, + }, + { + status: 400, + headers: { 'Access-Control-Allow-Origin': '*' }, + } + ); + } + + // Cache the module + moduleCache.set(cacheKey, { + module: toolModule, + expiresAt: now + CACHE_TTL_MS, + }); + } + + // Execute the tool with AI SDK execution context + const abortController = new AbortController(); + const executionContext = { + abortSignal: abortController.signal, + messages: [], + toolCallId: `exec_${Date.now()}`, + }; + + const result = await toolModule.execute(params || {}, executionContext); + + const response: ExecuteToolResponse = { + success: true, + output: result, + executionTimeMs: Date.now() - startTime, + }; + + return Response.json(response, { + headers: { 'Access-Control-Allow-Origin': '*' }, + }); + } catch (error) { + const response: ExecuteToolResponse = { + success: false, + error: error instanceof Error ? error.message : String(error), + executionTimeMs: Date.now() - startTime, + }; + + return Response.json(response, { + status: 500, + headers: { 'Access-Control-Allow-Origin': '*' }, + }); + } +} diff --git a/templates/vercel-executor/app/api/health/route.ts b/templates/vercel-executor/api/health.ts similarity index 53% rename from templates/vercel-executor/app/api/health/route.ts rename to templates/vercel-executor/api/health.ts index 21ec383..69afb55 100644 --- a/templates/vercel-executor/app/api/health/route.ts +++ b/templates/vercel-executor/api/health.ts @@ -1,36 +1,28 @@ /** - * Health Check Endpoint + * Health Check Endpoint (Deno Runtime) * * GET /api/health * Returns the health status of the executor */ -import { NextResponse } from 'next/server'; - -export const runtime = 'nodejs'; -export const dynamic = 'force-dynamic'; - interface HealthResponse { status: 'ok' | 'degraded' | 'error'; version?: string; info?: Record; } -export async function GET(): Promise> { - return NextResponse.json({ +export default function handler(_req: Request): Response { + const response: HealthResponse = { status: 'ok', version: '1.0.0', info: { - runtime: 'vercel-serverless', + runtime: 'deno', + httpImports: true, timestamp: new Date().toISOString(), }, - }); -} + }; -// Handle OPTIONS for CORS preflight -export async function OPTIONS(): Promise { - return new NextResponse(null, { - status: 200, + return Response.json(response, { headers: { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Methods': 'GET, OPTIONS', diff --git a/templates/vercel-executor/app/api/execute-tool/route.ts b/templates/vercel-executor/app/api/execute-tool/route.ts deleted file mode 100644 index 2d7e9ce..0000000 --- a/templates/vercel-executor/app/api/execute-tool/route.ts +++ /dev/null @@ -1,112 +0,0 @@ -/** - * Execute Tool Endpoint - * - * POST /api/execute-tool - * Executes a TPMJS tool with the provided parameters - */ - -import { type ExecuteToolRequest, executeTool } from '@/lib/executor'; -import { type NextRequest, NextResponse } from 'next/server'; - -export const runtime = 'nodejs'; -export const dynamic = 'force-dynamic'; -export const maxDuration = 60; // 60 seconds max - -/** - * Verify API key if configured - */ -function verifyApiKey(request: NextRequest): boolean { - const apiKey = process.env.EXECUTOR_API_KEY; - - // If no API key is configured, allow all requests - if (!apiKey) { - return true; - } - - const authHeader = request.headers.get('Authorization'); - if (!authHeader) { - return false; - } - - // Expect "Bearer " format - const [type, token] = authHeader.split(' '); - if (type !== 'Bearer' || token !== apiKey) { - return false; - } - - return true; -} - -/** - * Validate the request body - */ -function validateRequest(body: unknown): body is ExecuteToolRequest { - if (!body || typeof body !== 'object') { - return false; - } - - const req = body as Record; - - return ( - typeof req.packageName === 'string' && - req.packageName.length > 0 && - typeof req.name === 'string' && - req.name.length > 0 && - typeof req.params === 'object' && - req.params !== null - ); -} - -export async function POST(request: NextRequest): Promise { - // Verify API key if configured - if (!verifyApiKey(request)) { - return NextResponse.json( - { success: false, error: 'Unauthorized', executionTimeMs: 0 }, - { status: 401 } - ); - } - - try { - const body = await request.json(); - - // Validate request - if (!validateRequest(body)) { - return NextResponse.json( - { - success: false, - error: 'Invalid request. Required: packageName, name, params', - executionTimeMs: 0, - }, - { status: 400 } - ); - } - - // Execute the tool - const result = await executeTool(body); - - return NextResponse.json(result, { - status: result.success ? 200 : 500, - }); - } catch (error) { - return NextResponse.json( - { - success: false, - error: error instanceof Error ? error.message : 'Internal server error', - executionTimeMs: 0, - }, - { status: 500 } - ); - } -} - -// Handle OPTIONS for CORS preflight -export async function OPTIONS(): Promise { - return new NextResponse(null, { - status: 200, - headers: { - 'Access-Control-Allow-Origin': '*', - 'Access-Control-Allow-Methods': 'POST, OPTIONS', - 'Access-Control-Allow-Headers': 'Content-Type, Authorization', - }, - }); -} diff --git a/templates/vercel-executor/app/layout.tsx b/templates/vercel-executor/app/layout.tsx deleted file mode 100644 index 225b603..0000000 --- a/templates/vercel-executor/app/layout.tsx +++ /dev/null @@ -1,11 +0,0 @@ -export default function RootLayout({ - children, -}: { - children: React.ReactNode; -}) { - return ( - - {children} - - ); -} diff --git a/templates/vercel-executor/app/page.tsx b/templates/vercel-executor/app/page.tsx deleted file mode 100644 index 867088a..0000000 --- a/templates/vercel-executor/app/page.tsx +++ /dev/null @@ -1,20 +0,0 @@ -export default function Home() { - return ( -
-

TPMJS Executor

-

This is a TPMJS tool executor service.

-

Endpoints

-
    -
  • - GET /api/health - Health check -
  • -
  • - POST /api/execute-tool - Execute a tool -
  • -
-

- Documentation -

-
- ); -} diff --git a/templates/vercel-executor/lib/executor.ts b/templates/vercel-executor/lib/executor.ts deleted file mode 100644 index 0837479..0000000 --- a/templates/vercel-executor/lib/executor.ts +++ /dev/null @@ -1,101 +0,0 @@ -/** - * TPMJS Executor - Core Execution Logic - * - * This module handles dynamic import and execution of TPMJS tools. - * Tools are loaded from esm.sh and executed in a serverless environment. - */ - -export interface ExecuteToolRequest { - /** NPM package name, e.g., "@tpmjs/hello" */ - packageName: string; - /** Tool name within the package, e.g., "helloWorld" */ - name: string; - /** Package version, e.g., "1.0.0" or "latest" */ - version?: string; - /** Direct esm.sh URL override for the package */ - importUrl?: string; - /** Tool parameters to pass to execute() */ - params: Record; - /** Environment variables to inject during execution */ - env?: Record; -} - -export interface ExecuteToolResponse { - /** Whether the execution succeeded */ - success: boolean; - /** Tool output on success */ - output?: unknown; - /** Error message on failure */ - error?: string; - /** Execution duration in milliseconds */ - executionTimeMs: number; -} - -/** - * Build the esm.sh URL for a package - */ -function buildEsmUrl(packageName: string, version?: string): string { - const resolvedVersion = version || 'latest'; - return `https://esm.sh/${packageName}@${resolvedVersion}`; -} - -/** - * Execute a TPMJS tool - * - * @param request - Execution request with package name, tool name, and parameters - * @returns Execution result - */ -export async function executeTool(request: ExecuteToolRequest): Promise { - const startTime = Date.now(); - - try { - // Build the import URL - const importUrl = request.importUrl || buildEsmUrl(request.packageName, request.version); - - // Dynamically import the package from esm.sh - const module = await import(/* webpackIgnore: true */ importUrl); - - // Get the tool export - // Try the specific tool name first, then fall back to default export - const tool = module[request.name] || module.default; - - if (!tool) { - return { - success: false, - error: `Tool '${request.name}' not found in package '${request.packageName}'`, - executionTimeMs: Date.now() - startTime, - }; - } - - // Ensure the tool has an execute function - if (typeof tool.execute !== 'function') { - return { - success: false, - error: `Tool '${request.name}' does not have an execute() function`, - executionTimeMs: Date.now() - startTime, - }; - } - - // Inject environment variables if provided - if (request.env) { - for (const [key, value] of Object.entries(request.env)) { - process.env[key] = value; - } - } - - // Execute the tool - const output = await tool.execute(request.params); - - return { - success: true, - output, - executionTimeMs: Date.now() - startTime, - }; - } catch (error) { - return { - success: false, - error: error instanceof Error ? error.message : String(error), - executionTimeMs: Date.now() - startTime, - }; - } -} diff --git a/templates/vercel-executor/next.config.js b/templates/vercel-executor/next.config.js deleted file mode 100644 index 159f61c..0000000 --- a/templates/vercel-executor/next.config.js +++ /dev/null @@ -1,10 +0,0 @@ -/** @type {import('next').NextConfig} */ -const nextConfig = { - // Enable experimental features for better serverless performance - experimental: { - // Allow dynamic imports from esm.sh - serverActions: true, - }, -}; - -module.exports = nextConfig; diff --git a/templates/vercel-executor/package.json b/templates/vercel-executor/package.json index 13175e4..a29cd20 100644 --- a/templates/vercel-executor/package.json +++ b/templates/vercel-executor/package.json @@ -2,20 +2,11 @@ "name": "tpmjs-executor", "version": "1.0.0", "private": true, - "description": "TPMJS Tool Executor - Deploy your own executor for running TPMJS tools", + "description": "TPMJS Tool Executor - Deploy your own executor using Deno on Vercel", "scripts": { - "dev": "next dev", - "build": "next build", - "start": "next start" - }, - "dependencies": { - "next": "^15.0.0", - "react": "^19.0.0", - "react-dom": "^19.0.0" + "dev": "vercel dev" }, "devDependencies": { - "@types/node": "^20.0.0", - "@types/react": "^19.0.0", - "typescript": "^5.6.0" + "vercel": "^39.0.0" } } diff --git a/templates/vercel-executor/tsconfig.json b/templates/vercel-executor/tsconfig.json deleted file mode 100644 index e9ca8d0..0000000 --- a/templates/vercel-executor/tsconfig.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2022", - "lib": ["dom", "dom.iterable", "esnext"], - "allowJs": true, - "skipLibCheck": true, - "strict": true, - "noEmit": true, - "esModuleInterop": true, - "module": "esnext", - "moduleResolution": "bundler", - "resolveJsonModule": true, - "isolatedModules": true, - "jsx": "preserve", - "incremental": true, - "paths": { - "@/*": ["./*"] - } - }, - "include": ["**/*.ts", "**/*.tsx"], - "exclude": ["node_modules"] -} diff --git a/templates/vercel-executor/vercel.json b/templates/vercel-executor/vercel.json index afa288d..f86a91b 100644 --- a/templates/vercel-executor/vercel.json +++ b/templates/vercel-executor/vercel.json @@ -1,6 +1,10 @@ { "$schema": "https://openapi.vercel.sh/vercel.json", - "framework": "nextjs", + "functions": { + "api/**/*.ts": { + "runtime": "vercel-deno@3.0.0" + } + }, "headers": [ { "source": "/api/(.*)",