Skip to main content

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: