Custom Tool Development
Extend FAOS MCP Server with custom tools for your organization's specific needs.
Overviewβ
Custom tools allow you to:
- Integrate internal systems
- Add domain-specific capabilities
- Create organization-specific workflows
Tool Architectureβ
βββββββββββββββββββ ββββββββββββββββββββ βββββββββββββββββββ
β Claude Desktop ββββββΆβ FAOS MCP Server ββββββΆβ Custom Tool β
β β β β β Handler β
βββββββββββββββββββ ββββββββββββββββββββ ββββββββββ¬βββββββββ
β
βΌ
βββββββββββββββββββ
β Your Internal β
β System/API β
βββββββββββββββββββ
Creating a Custom Toolβ
Step 1: Define Tool Schemaβ
# custom_tools/jira_tool.py
from faos_mcp.tools import BaseTool, ToolParameter, ToolResponse
class JiraCreateIssueTool(BaseTool):
"""Create a Jira issue from within Claude."""
name = "jira_create_issue"
description = """
Create a new Jira issue. Use this when the user wants to:
- Create a bug report
- Log a task or ticket
- File a feature request
Do NOT use for:
- Updating existing issues (use jira_update_issue)
- Searching issues (use jira_search)
"""
parameters = [
ToolParameter(
name="project_key",
type="string",
description="Jira project key (e.g., 'PROJ')",
required=True,
),
ToolParameter(
name="summary",
type="string",
description="Issue title/summary",
required=True,
),
ToolParameter(
name="description",
type="string",
description="Detailed description (supports Jira markdown)",
required=False,
),
ToolParameter(
name="issue_type",
type="string",
description="Issue type: 'Bug', 'Task', 'Story', 'Epic'",
required=False,
default="Task",
),
ToolParameter(
name="priority",
type="string",
description="Priority: 'Highest', 'High', 'Medium', 'Low', 'Lowest'",
required=False,
default="Medium",
),
]
async def execute(self, **kwargs) -> ToolResponse:
"""Execute the tool."""
# Implementation in next step
pass
Step 2: Implement Tool Logicβ
# custom_tools/jira_tool.py (continued)
import httpx
from faos_mcp.tools import ToolResponse, ToolError
class JiraCreateIssueTool(BaseTool):
# ... schema from above ...
def __init__(self, jira_url: str, jira_token: str):
self.jira_url = jira_url
self.jira_token = jira_token
self.client = httpx.AsyncClient(
base_url=jira_url,
headers={
"Authorization": f"Bearer {jira_token}",
"Content-Type": "application/json",
},
)
async def execute(
self,
project_key: str,
summary: str,
description: str = "",
issue_type: str = "Task",
priority: str = "Medium",
) -> ToolResponse:
"""Create a Jira issue."""
payload = {
"fields": {
"project": {"key": project_key},
"summary": summary,
"description": {
"type": "doc",
"version": 1,
"content": [
{
"type": "paragraph",
"content": [{"type": "text", "text": description}],
}
],
},
"issuetype": {"name": issue_type},
"priority": {"name": priority},
}
}
try:
response = await self.client.post("/rest/api/3/issue", json=payload)
response.raise_for_status()
data = response.json()
return ToolResponse(
success=True,
data={
"issue_key": data["key"],
"issue_id": data["id"],
"url": f"{self.jira_url}/browse/{data['key']}",
"summary": summary,
},
message=f"Created Jira issue {data['key']}: {summary}",
)
except httpx.HTTPStatusError as e:
return ToolResponse(
success=False,
error=ToolError(
code="JIRA_API_ERROR",
message=f"Jira API error: {e.response.status_code}",
details=e.response.text,
),
)
Step 3: Register the Toolβ
# custom_tools/__init__.py
from .jira_tool import JiraCreateIssueTool
def register_custom_tools(registry, config):
"""Register all custom tools."""
if config.get("JIRA_URL") and config.get("JIRA_TOKEN"):
registry.register(
JiraCreateIssueTool(
jira_url=config["JIRA_URL"],
jira_token=config["JIRA_TOKEN"],
)
)
Step 4: Configureβ
{
"servers": {
"faos": {
"command": "faos-mcp",
"env": {
"FAOS_API_URL": "https://api.faosx.ai",
"FAOS_API_TOKEN": "your-token",
"FAOS_CUSTOM_TOOLS_PATH": "/path/to/custom_tools",
"JIRA_URL": "https://yourcompany.atlassian.net",
"JIRA_TOKEN": "your-jira-token"
}
}
}
}
Tool Design Best Practicesβ
Clear Descriptionsβ
Write descriptions that help Claude understand when to use the tool:
# Good
description = """
Create a new Jira issue. Use this when the user wants to:
- Create a bug report
- Log a task or ticket
- File a feature request
Do NOT use for:
- Updating existing issues (use jira_update_issue)
- Searching issues (use jira_search)
"""
# Bad
description = "Creates Jira issue"
Meaningful Parametersβ
# Good - Helps Claude provide correct values
ToolParameter(
name="priority",
type="string",
description="Priority level. Higher priority = more urgent.",
enum=["Highest", "High", "Medium", "Low", "Lowest"],
default="Medium",
)
# Bad - Unclear what values are valid
ToolParameter(
name="priority",
type="string",
description="Priority",
)
Helpful Responsesβ
# Good - Actionable information
return ToolResponse(
success=True,
data={
"issue_key": "PROJ-123",
"url": "https://jira.company.com/browse/PROJ-123",
},
message="Created issue PROJ-123. View at: https://jira.company.com/browse/PROJ-123",
)
# Bad - Minimal information
return ToolResponse(success=True, data={"id": "123"})
Error Handlingβ
# Categorize errors for better UX
try:
result = await self.api_call()
except AuthenticationError:
return ToolResponse(
success=False,
error=ToolError(
code="AUTH_ERROR",
message="Jira authentication failed. Please check your token.",
recoverable=True, # User can fix this
),
)
except RateLimitError:
return ToolResponse(
success=False,
error=ToolError(
code="RATE_LIMIT",
message="Jira rate limit exceeded. Please wait a moment.",
retry_after=60, # Suggest when to retry
),
)
Example Custom Toolsβ
Slack Notification Toolβ
class SlackNotifyTool(BaseTool):
name = "slack_notify"
description = "Send a notification to a Slack channel"
parameters = [
ToolParameter(name="channel", type="string", required=True),
ToolParameter(name="message", type="string", required=True),
ToolParameter(name="thread_ts", type="string", required=False),
]
async def execute(self, channel: str, message: str, thread_ts: str = None):
# Implementation
pass
Internal Wiki Search Toolβ
class WikiSearchTool(BaseTool):
name = "wiki_search"
description = "Search the internal company wiki/knowledge base"
parameters = [
ToolParameter(name="query", type="string", required=True),
ToolParameter(name="space", type="string", required=False),
ToolParameter(name="limit", type="integer", default=10),
]
async def execute(self, query: str, space: str = None, limit: int = 10):
# Implementation
pass
Database Query Toolβ
class SafeQueryTool(BaseTool):
name = "db_query"
description = "Run a read-only query against the analytics database"
parameters = [
ToolParameter(
name="query",
type="string",
description="SQL query (SELECT only, no mutations)",
required=True,
),
]
async def execute(self, query: str):
# Validate query is SELECT-only
if not query.strip().upper().startswith("SELECT"):
return ToolResponse(
success=False,
error=ToolError(
code="INVALID_QUERY",
message="Only SELECT queries are allowed",
),
)
# Execute with read-only connection
# Implementation
pass
Testing Custom Toolsβ
Unit Testsβ
# tests/test_jira_tool.py
import pytest
from custom_tools.jira_tool import JiraCreateIssueTool
@pytest.mark.asyncio
async def test_create_issue_success(mock_jira_api):
tool = JiraCreateIssueTool(
jira_url="https://test.atlassian.net",
jira_token="test-token",
)
result = await tool.execute(
project_key="TEST",
summary="Test issue",
description="Test description",
)
assert result.success is True
assert result.data["issue_key"] == "TEST-123"
@pytest.mark.asyncio
async def test_create_issue_auth_error(mock_jira_api_401):
tool = JiraCreateIssueTool(...)
result = await tool.execute(project_key="TEST", summary="Test")
assert result.success is False
assert result.error.code == "JIRA_API_ERROR"
Deploymentβ
Package Structureβ
custom_tools/
βββ __init__.py # Tool registration
βββ jira_tool.py # Jira integration
βββ slack_tool.py # Slack integration
βββ wiki_tool.py # Wiki search
βββ requirements.txt # Dependencies
βββ tests/
βββ test_jira.py
βββ test_slack.py
Docker with Custom Toolsβ
FROM ghcr.io/faosx/faos-mcp:latest
# Install custom tool dependencies
COPY custom_tools/requirements.txt /custom_tools/requirements.txt
RUN pip install -r /custom_tools/requirements.txt
# Copy custom tools
COPY custom_tools /custom_tools
# Configure custom tools path
ENV FAOS_CUSTOM_TOOLS_PATH=/custom_tools
Supportβ
For custom tool development:
- API Reference: docs.faosx.ai/api/tools
- Examples: github.com/faosx/custom-tool-examples
- Developer support: developers@faosx.ai