Skip to main content

AI Tools and MCP

This article covers HAWKI's function-calling tool system and the Model Context Protocol (MCP) integration. Together they are how HAWKI's agents gain access to capabilities beyond text generation — web search, knowledge base queries, code execution, and external tool servers.

ToolInterface — HAWKI's Tool Contract

App\Services\Ai\Tools\Contracts\ToolInterface extends Laravel AI's Tool interface with one HAWKI-specific method:

interface ToolInterface extends Tool
{
public function capability(): string|null;
public function name(): string;
}

capability() returns the WellKnownCapabilities key that this tool fulfils (e.g. 'web_search', 'knowledge_base'), or null when the tool does not map to any well-known capability. The agent layer uses this to locate the right tool when a model declares that it supports a capability.

AbstractTool — The Base Implementation

App\Services\Ai\Tools\AbstractTool is the base class for all HAWKI function-calling tools. Concrete subclasses implement __invoke() for their tool logic and provide name(), description(), and schema().

The invocation flow. Laravel AI calls handle(Request $request) on the tool. AbstractTool::handle():

  1. Validates the incoming arguments against the schema returned by schema() using JsonSchemaValidator.
  2. Enforces an optional per-instance call-count ceiling (set via setMaxRuns()).
  3. Dispatches __invoke() with the validated arguments via the ServiceLocator, which allows the container to inject additional dependencies (services, repositories) beyond the raw argument map.
  4. Converts the return value to a string — JSON-encoding arrays and objects automatically.
  5. Catches any exception, logs it, and returns a structured [ERROR] ... string that the AI model can understand and decide not to retry.

Example:

class GetCurrentWeatherTool extends AbstractTool
{
public function name(): string { return 'get_current_weather'; }
public function description(): string { return 'Returns the current weather for a city.'; }
public function capability(): string|null { return null; }

public function schema(JsonSchema $schema): array
{
return ['city' => $schema->string()->required()];
}

// 'city' is resolved from the validated argument map;
// WeatherClient is injected by the container
public function __invoke(WeatherClient $client, string $city): array
{
return $client->current($city);
}
}

Settings. AbstractTool also implements SettingsAwareToolInterface, which exposes a setSettings(array) / getSettings() pair. The agent layer calls setSettings() with per-model tool configuration before invoking the tool. Use this when a tool has configurable behaviour that varies per model assignment (e.g. API keys, result counts).

Database-Backed Tool Records

Tools are not just PHP classes — they are persisted in the database so operators can control which tools are enabled, assigned to which models, and with what settings.

AiTool (App\Models\Ai\AiTool) is the Eloquent model for a tool record. It carries the tool's name, type (function or mcp), capability key, settings, and an active flag.

AiToolCapability is a related model that tracks which capability keys are available for each tool.

Tools stored in the database are exposed via the ai-tools JSON:API resource, which allows the admin UI to manage tool-model assignments.

Registering Function Tools

Function tools (PHP classes) are registered with the service container using the ToolInterface::class tag:

// In your ServiceProvider::register()
$this->app->tag([GetCurrentWeatherTool::class], ToolInterface::class);

FunctionToolSyncer collects all tagged ToolInterface implementations and upserts each into the ai_tools table via AiToolRepository::upsertFunction(). This runs at deployment time via ai:tools:sync.

Tools no longer registered are not automatically removed — deletion is a deliberate manual step to avoid accidental data loss.

Extension point

The container tag pattern is plugin-ready today. $this->app->tag([MyTool::class], ToolInterface::class) in your plugin's ServiceProvider::register() is sufficient to register a custom tool. FunctionToolSyncer and tag-based discovery already support this without any core API change. See Plugin System Preview.

MCP Integration

Model Context Protocol (MCP) is an open protocol for connecting AI agents to external tool servers. HAWKI integrates with MCP servers for tools that run outside the PHP process — for example, a RAG server, a web search service, or any third-party tool endpoint.

McpServer — The Persisted Record

App\Models\Ai\McpServer is the Eloquent model for a registered MCP server. It stores the server URL, transport type (sse or rest), authentication config, and online status. Records are managed via the mcp-servers JSON:API resource and the ai:tools:mcp:add / mcp:remove artisan commands.

HawkiMcpClient — Lazy Session Management

App\Services\Ai\Tools\Mcp\HawkiMcpClient wraps a single MCP ClientSession with two layers of laziness:

  • Session creation — the underlying ClientSession is created on first use via an injected factory closure and reused for the lifetime of the instance.
  • Tool caching — tool definitions fetched via listToolDefinitions() are cached in a RewindableGenerator after the first call. Repeated calls within the same request do not re-query the MCP server.

Key methods:

  • ping() — used by McpServerStatusUpdater for health checks. Returns false on failure without throwing.
  • callTool(name, arguments) — invokes a named tool and returns a CallToolResult. Errors are logged before re-throwing.
  • listToolDefinitions() — yields McpToolDefinition value objects for all tools the server advertises. An optional hawkiCapability field on each raw tool definition maps the tool to a HAWKI capability key.

Instances are managed as lazy singletons per McpServer via a LazySingletonList binding in AiServiceProvider.

McpToolSyncer — Discovery and Sync

App\Services\Ai\Tools\Mcp\McpToolSyncer runs at deployment time (via ai:tools:sync --mcp-only) to discover and persist MCP tools:

  1. Iterates all registered McpServer records whose status is ONLINE.
  2. For each server, opens a HawkiMcpClient and calls listToolDefinitions().
  3. Upserts each tool into the ai_tools table via AiToolRepository::upsertMcp(), collecting the IDs of all synced tools.
  4. After all tools for a server are processed, calls AiToolRepository::removeAllMcpToolsOf() to delete rows for tools no longer advertised by the server.

Individual tool failures are caught without aborting the rest of the server's sync. Server-level failures are similarly isolated.

BeforeCallingMcpToolFilterEvent — Intercept and Mock

BeforeCallingMcpToolFilterEvent is a filter event (DispatchableFilter) that fires immediately before any MCP tool call. Listeners receive the tool arguments, the AiTool record, and the HawkiMcpClient, and can short-circuit the real MCP call by calling setResult() with a synthetic response.

This is essential for testing — without this hook you would need a real MCP server running to test agent behaviour. With it, a test listener can inject a predetermined result:

Event::listen(BeforeCallingMcpToolFilterEvent::class, function ($event) {
if ($event->getTool()->name === 'my_mcp_tool') {
$event->setResult(json_encode(['answer' => 'mocked value']));
}
});

Other filter events in the tool layer:

EventWhen it fires
ToolByNameResolvedFilterEventAfter a tool is resolved by name — allows replacement
ToolForCapabilityResolvedFilterEventAfter a tool is resolved for a capability — allows replacement
NativeToolResolvedFilterEventAfter a native provider tool is resolved
McpToolCalledFilterEventAfter an MCP tool call completes

Configuration: config/tools.php

config/tools.php is the deployment-level configuration file for function tools and MCP servers. It is read only by ai:tools:sync — not at runtime.

return [
// Function-calling tools (PHP classes)
'available_tools' => [
\App\Services\Ai\Tools\Implementations\TestTool::class,
// Add your tool classes here, then run: ai:tools:sync --function-only
],

// MCP servers
'mcp_servers' => [
'my-rag-server' => [
'type' => 'sse',
'url' => env('RAG_MCP_URL', 'http://localhost:8080/mcp'),
// 'api_key' => env('RAG_MCP_API_KEY'), // sent as Bearer token
],
],
];

Tool names from MCP servers are prefixed with the server's label key to avoid conflicts across servers (e.g. a tool named search from a server registered as hawki-rag becomes hawki-rag-search).

The ai:tools:sync Command

ai:tools:sync is the single command that synchronises all tool definitions from config to the database. It can be targeted with flags:

  • --function-only — sync only PHP function tools.
  • --mcp-only — sync only MCP server tools.

Run this command at deployment time after any change to config/tools.php or after registering a new tool class.

Key Classes

ClassLocationRole
ToolInterfaceTools/Contracts/HAWKI tool contract (extends Laravel AI's Tool)
AbstractToolTools/Base class with validation, settings, error handling
FunctionToolSyncerTools/Discovers and upserts tagged tool classes
AiToolapp/Models/Ai/Eloquent model for a tool record
HawkiMcpClientTools/Mcp/Lazy MCP session with tool caching
McpToolSyncerTools/Mcp/MCP tool discovery and DB sync
McpClientFactoryTools/Mcp/Creates HawkiMcpClient instances
BeforeCallingMcpToolFilterEventTools/LaravelAi/Events/Pre-call intercept / mock hook
ToolByNameResolvedFilterEventTools/LaravelAi/Events/Post-resolution filter for named tools
ToolForCapabilityResolvedFilterEventTools/LaravelAi/Events/Post-resolution filter for capability tools
AiToolRepositoryTools/Repositories/Tool CRUD (function + MCP upserts)
McpServerRepositoryTools/Repositories/MCP server record access