Custom MCP Tools
Custom MCP (Model Context Protocol) tools allow you to extend Fluxbase’s AI capabilities with domain-specific functionality. Write tools in TypeScript, deploy them to your Fluxbase instance, and they become available to all AI chatbots configured to use them.
Overview
Section titled “Overview”MCP tools are functions that AI assistants can invoke during conversations. Custom tools let you:
- Integrate external APIs (weather, payments, notifications)
- Implement business logic accessible to AI
- Create domain-specific data transformations
- Build custom validation and processing pipelines
Custom resources provide read-only data that AI can access during conversations, such as configuration, analytics, or dynamic content.
Quick Start
Section titled “Quick Start”1. Create a Custom Tool
Section titled “1. Create a Custom Tool”Create a TypeScript file with your tool implementation:
// @fluxbase:description Get orders for a user
export async function handler( args: { user_id: string; limit?: number }, fluxbase: any, // User-scoped client (respects RLS) fluxbaseService: any, // Service-scoped client (bypasses RLS) utils: any // Tool metadata and helpers) { const { user_id, limit = 10 } = args;
// Same signature as edge functions: handler(args, fluxbase, fluxbaseService, utils) const { data: orders } = await fluxbase .from("orders") .select("id, status, total, created_at") .eq("user_id", user_id) .order("created_at", { ascending: false }) .limit(limit) .execute();
return { content: [{ type: "text", text: JSON.stringify(orders, null, 2) }] };}2. Deploy via CLI
Section titled “2. Deploy via CLI”# Create the toolfluxbase mcp tools create get_user_orders --code ./get_user_orders.ts
# Or sync a directory of tools (all .ts files become tools)fluxbase mcp tools sync --dir ./mcp-tools3. Use in Chatbots
Section titled “3. Use in Chatbots”Configure your chatbot to use the custom tool. Custom tools use a colon-separated naming format:
- Default namespace:
custom:tool_name - Other namespaces:
custom:namespace:tool_name
/** * @fluxbase:mcp-tools custom:get_user_orders,query_table */
export default `You are an order management assistant.You can look up user orders and query tables.`;For tools in non-default namespaces:
/** * @fluxbase:mcp-tools custom:production:get_user_orders,custom:analytics:track_event */
export default `You are a production assistant with access to production tools.`;Tool Annotations
Section titled “Tool Annotations”All annotations are optional. The @fluxbase: prefix is consistent with edge functions and jobs.
| Annotation | Description | Default |
|---|---|---|
@fluxbase:name | Tool name | Filename (e.g., weather_forecast.ts → weather_forecast) |
@fluxbase:namespace | Tool namespace for isolation | default |
@fluxbase:description | Human-readable description for AI | None |
@fluxbase:scopes | Additional MCP scopes | execute:custom |
@fluxbase:timeout | Execution timeout in seconds | 30 |
@fluxbase:memory | Memory limit in MB | 128 |
@fluxbase:allow-net | Allow network access | false |
@fluxbase:allow-env | Allow secrets/environment access | false |
Input Schema
Section titled “Input Schema”Define input validation using JSON Schema:
// Via API or dashboard, set input_schema:{ "type": "object", "properties": { "location": { "type": "string", "description": "City name or coordinates" }, "days": { "type": "number", "default": 3, "minimum": 1, "maximum": 14 } }, "required": ["location"]}Handler Signature
Section titled “Handler Signature”MCP tools use the same handler signature as edge functions and jobs:
handler(args, fluxbase, fluxbaseService, utils)| Parameter | Description |
|---|---|
args | Input arguments passed to the tool |
fluxbase | User-scoped Fluxbase client (respects RLS) |
fluxbaseService | Service-scoped Fluxbase client (bypasses RLS) |
utils | Tool metadata and helpers |
Utils Object
Section titled “Utils Object”interface ToolUtils { // Tool metadata tool_name: string; namespace: string;
// User information user_id: string; user_email: string; user_role: string; scopes: string[];
// Secrets accessor (requires allow_env permission) secrets: { get(name: string): string | undefined; };
// Environment access (requires allow_env permission) env: { get(name: string): string | undefined; };
// AI capabilities - access AI completions and embeddings ai: { // Chat completion chat(options: { messages: Array<{ role: string; content: string }>; provider?: string; // AI provider name (uses default if not specified) model?: string; // Model override maxTokens?: number; // Max response tokens (default: 1024) temperature?: number; // 0-1, default: 0.7 }): Promise<{ content: string; model: string; finish_reason?: string; usage?: { prompt_tokens: number; completion_tokens: number; total_tokens: number }; }>;
// Generate embeddings embed(options: { text: string; provider?: string; // Embedding provider }): Promise<{ embedding: number[]; model: string; }>;
// List available providers listProviders(): Promise<{ providers: Array<{ name: string; type: string; model: string; enabled: boolean }>; default: string; }>; };}
interface FluxbaseClient { // Query builder from(table: string): QueryBuilder; insert(table: string, data: any): InsertBuilder; update(table: string, data: any): UpdateBuilder; delete(table: string): DeleteBuilder;
// RPC calls rpc(functionName: string, params?: object): Promise<{ data: any; error: null }>;
// Storage operations storage: { list(bucket: string, options?: { prefix?: string; limit?: number }): Promise<any>; download(bucket: string, path: string): Promise<Response>; upload(bucket: string, path: string, file: any, options?: { contentType?: string }): Promise<any>; remove(bucket: string, paths: string | string[]): Promise<any>; getPublicUrl(bucket: string, path: string): string; };
// Edge functions functions: { invoke(name: string, options?: { body?: any; headers?: object }): Promise<{ data: any; error: null }>; };}Accessing Fluxbase Data
Section titled “Accessing Fluxbase Data”export async function handler(args: { userId: string }, fluxbase, fluxbaseService, utils) { // Query using user context (respects RLS - user can only see their own data) const { data: userData } = await fluxbase .from("profiles") .select("id, name, email") .eq("id", args.userId) .single() .execute();
// Query using service context (bypasses RLS - admin access) const { data: allUsers } = await fluxbaseService .from("profiles") .select("id, name") .limit(10) .execute();
return { content: [{ type: "text", text: JSON.stringify({ user: userData, recentUsers: allUsers }) }] };}Insert, Update, Delete
Section titled “Insert, Update, Delete”export async function handler(args: { name: string; email: string }, fluxbase, fluxbaseService, utils) { // Insert a new record const { data: created } = await fluxbaseService .insert("users", { name: args.name, email: args.email }) .select("id, name, email") .execute();
// Update a record const { data: updated } = await fluxbaseService .update("users", { last_login: new Date().toISOString() }) .eq("email", args.email) .execute();
// Delete a record const { data: deleted } = await fluxbaseService .delete("users") .eq("id", args.userId) .execute();
return created;}Custom Resources
Section titled “Custom Resources”Resources provide read-only data to AI assistants. The URI defaults to fluxbase://custom/{name} based on filename.
Resources use the same handler signature:
// @fluxbase:description Real-time analytics summary
export async function handler(params: {}, fluxbase, fluxbaseService, utils) { const { data } = await fluxbase .from("analytics_events") .select("*") .execute();
return [ { type: "text", text: JSON.stringify({ total_events: data.length, last_updated: new Date().toISOString() }) } ];}Template Resources
Section titled “Template Resources”For parameterized URIs, specify a custom URI with {param} placeholders. Templates are auto-detected:
// @fluxbase:uri fluxbase://custom/users/{id}/profile
export async function handler(params: { id: string }, fluxbase, fluxbaseService, utils) { const { data: user } = await fluxbase .from("users") .select("*") .eq("id", params.id) .single() .execute();
return [{ type: "text", text: JSON.stringify(user) }];}API Reference
Section titled “API Reference”List Tools
Section titled “List Tools”GET /api/v1/mcp/toolsCreate Tool
Section titled “Create Tool”POST /api/v1/mcp/toolsContent-Type: application/json
{ "name": "weather_forecast", "namespace": "default", "description": "Get weather forecast", "code": "export async function handler...", "input_schema": { "type": "object", ... }, "required_scopes": ["execute:custom"], "timeout_seconds": 30, "memory_limit_mb": 128, "allow_net": true, "allow_env": false}Sync Tool (Upsert)
Section titled “Sync Tool (Upsert)”POST /api/v1/mcp/tools/syncContent-Type: application/json
{ "name": "weather_forecast", "namespace": "default", "code": "...", "upsert": true}Test Tool
Section titled “Test Tool”POST /api/v1/mcp/tools/:id/testContent-Type: application/json
{ "args": { "location": "New York" }}CLI Commands
Section titled “CLI Commands”# List toolsfluxbase mcp tools list
# Get tool detailsfluxbase mcp tools get weather_forecast
# Create toolfluxbase mcp tools create weather_forecast --code ./weather.ts
# Update toolfluxbase mcp tools update weather_forecast --code ./weather.ts
# Delete toolfluxbase mcp tools delete weather_forecast
# Sync directoryfluxbase mcp tools sync --dir ./mcp-tools --namespace production
# Test toolfluxbase mcp tools test weather_forecast --args '{"location": "NYC"}'
# Resourcesfluxbase mcp resources listfluxbase mcp resources create analytics --uri "fluxbase://custom/analytics" --code ./analytics.tsfluxbase mcp resources sync --dir ./mcp-resourcesSecurity
Section titled “Security”Deno Sandbox
Section titled “Deno Sandbox”Custom tools run in a sandboxed Deno environment with explicit permissions:
- Network (
allow_net): Required for external API calls - Environment (
allow_env): Access to environment variables and secrets - Read (
allow_read): File system read access - Write (
allow_write): File system write access
Scopes
Section titled “Scopes”Tools require the execute:custom scope plus any additional scopes you specify. Users must have these scopes to invoke the tool.
Secrets
Section titled “Secrets”Access secrets securely via context.secrets.get("SECRET_NAME"). Secrets are:
- Encrypted at rest
- Never exposed in logs
- Only available when
allow_envis enabled
Integration with Chatbots
Section titled “Integration with Chatbots”Custom tools are automatically available to chatbots. Configure which tools a chatbot can use:
/** * @fluxbase:mcp-tools custom:check_order_status,query_table */export default `You are a customer service assistant.`;Tool Naming Format
Section titled “Tool Naming Format”Tools use a colon-separated naming format to distinguish custom tools from built-in tools and to include namespace information:
- Default namespace:
custom:{name}(e.g.,custom:check_order_status) - Named namespace:
custom:{namespace}:{name}(e.g.,custom:production:check_order_status)
So check_order_status.ts in the default namespace becomes custom:check_order_status, while one in the production namespace becomes custom:production:check_order_status.
/** * @fluxbase:mcp-tools custom:production:get_inventory,custom:production:update_stock */export default `You are an inventory management assistant with production access.`;AI-Powered Tools
Section titled “AI-Powered Tools”Custom MCP tools can leverage AI capabilities via utils.ai to create intelligent, context-aware functionality.
AI Chat Completions
Section titled “AI Chat Completions”// @fluxbase:description Analyze customer support ticket and suggest response// @fluxbase:allow-net
export async function handler( args: { ticket_id: string }, fluxbase, fluxbaseService, utils) { // Get the support ticket const { data: ticket } = await fluxbase .from("support_tickets") .select("subject, description, customer_email") .eq("id", args.ticket_id) .single() .execute();
if (!ticket) { return { content: [{ type: "text", text: "Ticket not found" }], isError: true }; }
// Use AI to analyze and generate response const response = await utils.ai.chat({ messages: [ { role: "system", content: "You are a helpful customer support assistant. Analyze the ticket and suggest a professional response." }, { role: "user", content: `Subject: ${ticket.subject}\n\nDescription: ${ticket.description}` } ], maxTokens: 500, temperature: 0.7 });
return { content: [{ type: "text", text: `Suggested response for ticket ${args.ticket_id}:\n\n${response.content}` }] };}AI Embeddings for Semantic Search
Section titled “AI Embeddings for Semantic Search”// @fluxbase:description Search knowledge base using semantic similarity// @fluxbase:allow-net
export async function handler( args: { query: string; limit?: number }, fluxbase, fluxbaseService, utils) { // Generate embedding for the search query const { embedding } = await utils.ai.embed({ text: args.query });
// Use pgvector to find similar documents const { data: results } = await fluxbaseService.rpc("match_documents", { query_embedding: embedding, match_threshold: 0.7, match_count: args.limit || 5 });
return { content: [{ type: "text", text: JSON.stringify(results, null, 2) }] };}Combining AI with Business Logic
Section titled “Combining AI with Business Logic”// @fluxbase:description Analyze recent orders and provide insights// @fluxbase:allow-net
export async function handler( args: { days?: number }, fluxbase, fluxbaseService, utils) { const days = args.days || 7; const since = new Date(); since.setDate(since.getDate() - days);
// Get recent orders const { data: orders } = await fluxbaseService .from("orders") .select("id, total, status, created_at") .gte("created_at", since.toISOString()) .execute();
// Calculate stats const totalRevenue = orders.reduce((sum, o) => sum + o.total, 0); const statusCounts = orders.reduce((acc, o) => { acc[o.status] = (acc[o.status] || 0) + 1; return acc; }, {});
// Use AI to generate insights const response = await utils.ai.chat({ messages: [ { role: "system", content: "You are a business analyst. Provide brief, actionable insights." }, { role: "user", content: `Analyze these ${days}-day order stats:- Total orders: ${orders.length}- Total revenue: $${totalRevenue.toFixed(2)}- Status breakdown: ${JSON.stringify(statusCounts)}` } ], maxTokens: 300 });
return { content: [{ type: "text", text: `Order Analysis (${days} days):\n\n${response.content}` }] };}Best Practices
Section titled “Best Practices”- Validate inputs - Use JSON Schema for input validation
- Handle errors gracefully - Return
isError: truewith helpful messages - Keep tools focused - One tool, one responsibility
- Use timeouts - Set appropriate timeouts for external API calls
- Secure secrets - Never hardcode API keys; use the secrets system
- Test thoroughly - Use the test endpoint before deploying to production
- Document tools - Clear descriptions help AI use tools correctly
- Use AI judiciously - AI calls add latency; use for complex reasoning, not simple lookups
Example: Complete Integration
Section titled “Example: Complete Integration”// @fluxbase:description Check the status of a customer order
export async function handler(args: { order_id: string }, fluxbase, fluxbaseService, utils) { const { order_id } = args;
// Validate input if (!order_id || !order_id.match(/^ORD-\d+$/)) { return { content: [{ type: "text", text: "Invalid order ID format. Expected: ORD-XXXXX" }], isError: true }; }
// Query Fluxbase const { data: orders } = await fluxbase .from("orders") .select("id, status, created_at, total, items") .eq("id", order_id) .execute();
if (!orders || orders.length === 0) { return { content: [{ type: "text", text: `Order ${order_id} not found` }], isError: true }; }
const order = orders[0];
return { content: [{ type: "text", text: `Order ${order.id}:- Status: ${order.status}- Created: ${order.created_at}- Total: $${order.total}- Items: ${order.items.length} item(s)` }] };}Deploy and test:
fluxbase mcp tools create check_order_status --code ./check_order_status.tsfluxbase mcp tools test check_order_status --args '{"order_id": "ORD-12345"}'