Skip to content

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.

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.

Create a TypeScript file with your tool implementation:

get_user_orders.ts
// @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) }]
};
}
Terminal window
# Create the tool
fluxbase 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-tools

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
order_assistant.ts
/**
* @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:

production_assistant.ts
/**
* @fluxbase:mcp-tools custom:production:get_user_orders,custom:analytics:track_event
*/
export default `You are a production assistant with access to production tools.`;

All annotations are optional. The @fluxbase: prefix is consistent with edge functions and jobs.

AnnotationDescriptionDefault
@fluxbase:nameTool nameFilename (e.g., weather_forecast.tsweather_forecast)
@fluxbase:namespaceTool namespace for isolationdefault
@fluxbase:descriptionHuman-readable description for AINone
@fluxbase:scopesAdditional MCP scopesexecute:custom
@fluxbase:timeoutExecution timeout in seconds30
@fluxbase:memoryMemory limit in MB128
@fluxbase:allow-netAllow network accessfalse
@fluxbase:allow-envAllow secrets/environment accessfalse

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"]
}

MCP tools use the same handler signature as edge functions and jobs:

handler(args, fluxbase, fluxbaseService, utils)
ParameterDescription
argsInput arguments passed to the tool
fluxbaseUser-scoped Fluxbase client (respects RLS)
fluxbaseServiceService-scoped Fluxbase client (bypasses RLS)
utilsTool metadata and helpers
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 }>;
};
}
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 }) }]
};
}
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;
}

Resources provide read-only data to AI assistants. The URI defaults to fluxbase://custom/{name} based on filename.

Resources use the same handler signature:

analytics_summary.ts
// @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()
})
}
];
}

For parameterized URIs, specify a custom URI with {param} placeholders. Templates are auto-detected:

user_profile.ts
// @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) }];
}
Terminal window
GET /api/v1/mcp/tools
Terminal window
POST /api/v1/mcp/tools
Content-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
}
Terminal window
POST /api/v1/mcp/tools/sync
Content-Type: application/json
{
"name": "weather_forecast",
"namespace": "default",
"code": "...",
"upsert": true
}
Terminal window
POST /api/v1/mcp/tools/:id/test
Content-Type: application/json
{
"args": { "location": "New York" }
}
Terminal window
# List tools
fluxbase mcp tools list
# Get tool details
fluxbase mcp tools get weather_forecast
# Create tool
fluxbase mcp tools create weather_forecast --code ./weather.ts
# Update tool
fluxbase mcp tools update weather_forecast --code ./weather.ts
# Delete tool
fluxbase mcp tools delete weather_forecast
# Sync directory
fluxbase mcp tools sync --dir ./mcp-tools --namespace production
# Test tool
fluxbase mcp tools test weather_forecast --args '{"location": "NYC"}'
# Resources
fluxbase mcp resources list
fluxbase mcp resources create analytics --uri "fluxbase://custom/analytics" --code ./analytics.ts
fluxbase mcp resources sync --dir ./mcp-resources

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

Tools require the execute:custom scope plus any additional scopes you specify. Users must have these scopes to invoke the tool.

Access secrets securely via context.secrets.get("SECRET_NAME"). Secrets are:

  • Encrypted at rest
  • Never exposed in logs
  • Only available when allow_env is enabled

Custom tools are automatically available to chatbots. Configure which tools a chatbot can use:

my_chatbot.ts
/**
* @fluxbase:mcp-tools custom:check_order_status,query_table
*/
export default `You are a customer service assistant.`;

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.

production_chatbot.ts
/**
* @fluxbase:mcp-tools custom:production:get_inventory,custom:production:update_stock
*/
export default `You are an inventory management assistant with production access.`;

Custom MCP tools can leverage AI capabilities via utils.ai to create intelligent, context-aware functionality.

smart_support.ts
// @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}`
}]
};
}
semantic_search.ts
// @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)
}]
};
}
analyze_orders.ts
// @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}`
}]
};
}
  1. Validate inputs - Use JSON Schema for input validation
  2. Handle errors gracefully - Return isError: true with helpful messages
  3. Keep tools focused - One tool, one responsibility
  4. Use timeouts - Set appropriate timeouts for external API calls
  5. Secure secrets - Never hardcode API keys; use the secrets system
  6. Test thoroughly - Use the test endpoint before deploying to production
  7. Document tools - Clear descriptions help AI use tools correctly
  8. Use AI judiciously - AI calls add latency; use for complex reasoning, not simple lookups
check_order_status.ts
// @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:

Terminal window
fluxbase mcp tools create check_order_status --code ./check_order_status.ts
fluxbase mcp tools test check_order_status --args '{"order_id": "ORD-12345"}'