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.

Annotation Description Default
@fluxbase:name Tool name Filename (e.g., weather_forecast.tsweather_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

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)
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
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 MCP tools are stored as single files, so any code shared across tools — a constants map, a date-range parser, an HTTP client wrapper — must be inlined at sync time. The fluxbase mcp tools sync command does this automatically when it finds a _shared/ directory next to your tool files. (This is the same convention used by edge functions and jobs.)

fluxbase/mcp-tools/
├── _shared/
│ ├── countries.ts # country-name → ISO code map
│ └── date_range.ts # parses "last 7 days", "this month", etc.
├── search_visits.ts # imports from _shared/
└── aggregate_visits.ts # imports from _shared/

Reference shared modules with a relative _shared/... path:

search_visits.ts
import { COUNTRIES } from "./_shared/countries.ts";
import { parseDateRange } from "./_shared/date_range.ts";
export default async function handler(args: any) {
const range = parseDateRange(args.timeframe);
const iso = COUNTRIES[args.country.toLowerCase()];
// ...
}

Run sync as usual — files with imports are bundled locally (requires Deno on your PATH) and the bundled output is what’s stored:

Terminal window
fluxbase mcp tools sync --dir ./fluxbase/mcp-tools
# Bundling search_visits.ts... 4.2 KB → 18.7 KB
# Bundling aggregate_visits.ts... 3.8 KB → 17.9 KB

If Deno isn’t installed locally, sync falls back to posting raw source; tools with imports will then fail at runtime, so install Deno (brew install deno) when using _shared/.

  • The stored tool body is the bundled output, not the original. Edit source in git and re-sync — direct edits via the admin UI will be overwritten on the next sync.
  • Bundling is opt-in per file: tools without import statements are uploaded verbatim, exactly as before.

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