tpmjs/templates/vercel-executor/app/api/execute-tool/route.ts
Ajax Davis bc36d366bc feat: add hot-swappable executor support for collections and agents
- Add executor configuration to Collection and Agent models in Prisma schema
- Create ExecutorConfigPanel component for selecting default or custom executors
- Add executor resolution logic with cascade (Agent → Collection → System Default)
- Create /api/executors/verify endpoint to test custom executor connectivity
- Add executor documentation page at /docs/executors with API specification
- Create deployable Vercel executor template in templates/vercel-executor/
- Update MCP handlers and agent tool execution to use configurable executors
- Add executor types and schemas to @tpmjs/types package

Users can now deploy their own executor instances and configure collections
or agents to use custom executors instead of the TPMJS default executor.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-09 00:19:22 +10:00

112 lines
2.6 KiB
TypeScript

/**
* 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 <api-key>" 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<string, unknown>;
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<NextResponse> {
// 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<NextResponse> {
return new NextResponse(null, {
status: 200,
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'POST, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
},
});
}