AI & Agents
•7 min read
MCP Integration
Bridge MCP servers into Directive with constraint-driven tool access control.
Setup
The createMCPAdapter function connects to MCP servers and provides a Directive plugin for tool constraints, resource syncing, and approval workflows:
Import Path
The @directive-run/ai package includes MCP support. See the installation docs for setup.
import { createMCPAdapter } from '@directive-run/ai';
import { createModule, createSystem, t } from '@directive-run/core';
const adapter = createMCPAdapter({
servers: [
// Local server via stdin/stdout
{
name: 'filesystem',
transport: 'stdio',
command: 'mcp-server-filesystem',
args: ['--root', '/workspace'],
},
// Remote server via Server-Sent Events
{
name: 'github',
transport: 'sse',
url: 'https://mcp.github.com',
auth: { type: 'bearer', token: process.env.GITHUB_TOKEN },
},
],
});
// Register the adapter as a Directive plugin for constraint integration
const system = createSystem({
module: myModule,
plugins: [adapter.plugin],
});
// Open connections to all configured servers
await adapter.connect();
In production, provide a real MCP client via clientFactory. Without it, a stub client is used for development:
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
// Provide a real MCP client for production (without this, a stub is used)
const adapter = createMCPAdapter({
servers: [...],
clientFactory: (config) => new Client(config),
});
Calling Tools
Call MCP tools with automatic constraint checking (rate limits, approval, argument size limits):
// Call a tool with constraint checking (rate limits, approval, size limits)
const result = await adapter.callTool('filesystem', 'read_file', {
path: '/workspace/config.json',
}, system.facts.$store.toObject());
// Iterate over the response – content can be text, image, or resource
for (const content of result.content) {
if (content.type === 'text') {
console.log(content.text);
}
}
// Bypass all constraints for trusted internal calls
const raw = await adapter.callToolDirect('filesystem', 'read_file', {
path: '/workspace/config.json',
});
Tool Constraints
Control tool access with per-tool constraints:
const adapter = createMCPAdapter({
servers: [
{ name: 'fs', transport: 'stdio', command: 'mcp-server-filesystem' },
],
// Define per-tool access rules
toolConstraints: {
// Require human approval before any write operation
'fs.write_file': {
requireApproval: true,
maxArgSize: 10000, // Reject arguments larger than 10KB
timeout: 30000, // 30s timeout per call
},
// Throttle read operations to prevent abuse
'fs.read_file': {
rateLimit: 60, // Max 60 calls per minute
},
// Only allow deletes for admin users
'fs.delete_file': {
requireApproval: true,
when: (facts, args) => facts.userRole === 'admin',
},
},
});
Approval Workflow
When a tool has requireApproval: true, calls pause until approved:
const adapter = createMCPAdapter({
servers: [...],
toolConstraints: {
'fs.write_file': { requireApproval: true },
},
approvalTimeoutMs: 60000, // Fail after 60s with no decision (default: 5 minutes)
events: {
// Fires when a constrained tool call needs human approval
onApprovalRequest: (request) => {
console.log(`Approval needed: ${request.server}.${request.tool}`);
console.log('Arguments:', request.args);
notifyApprover(request); // Push to your approval UI
},
onApprovalResolved: (requestId, approved) => {
console.log(`${requestId}: ${approved ? 'approved' : 'rejected'}`);
},
},
});
// Wire these into your approval UI handler
adapter.approve(requestId);
adapter.reject(requestId, 'Not authorized');
// Query all pending approvals at any time
const pending = adapter.getPendingApprovals();
Resource Syncing
Map MCP resources to Directive facts. Resources can be polled, subscribed to, or synced manually:
const adapter = createMCPAdapter({
servers: [
{ name: 'fs', transport: 'stdio', command: 'mcp-server-filesystem' },
],
// Map MCP resources to Directive facts
resourceMappings: [
{
pattern: 'file://*.json',
factKey: 'jsonFiles',
mode: 'poll', // Check for changes on a timer
pollInterval: 5000, // Sync every 5 seconds
transform: (content) => JSON.parse(content), // Parse raw content into objects
},
{
pattern: /^file:\/\/.*\.md$/,
factKey: 'markdownFiles',
mode: 'subscribe', // Receive real-time push updates
},
{
pattern: 'file:///workspace/config.yaml',
factKey: 'config',
mode: 'manual', // Only sync on explicit call
},
],
});
// Trigger a manual sync for resources with mode: 'manual'
await adapter.syncResources(system.facts.$store.toObject());
// Read a single resource directly by URI
const resource = await adapter.readResource('fs', 'file:///workspace/README.md');
Server Management
Connect, disconnect, and monitor individual servers:
// Manage individual server connections
await adapter.connectServer('github');
await adapter.disconnectServer('github');
// Inspect a single server
const status = adapter.getServerStatus('filesystem');
console.log(status?.status); // 'disconnected' | 'connecting' | 'connected' | 'error'
console.log(status?.tools); // Tools exposed by this server
console.log(status?.resources); // Resources exposed by this server
// Enumerate all server statuses at once
const all = adapter.getAllServerStatuses();
for (const [name, s] of all) {
console.log(`${name}: ${s.status}`);
}
Discovery
List available tools and resources across all connected servers:
// List all tools exposed by connected servers
const tools = adapter.getTools();
for (const [server, serverTools] of tools) {
for (const tool of serverTools) {
console.log(`${server}.${tool.name}: ${tool.description}`);
}
}
// List all resources exposed by connected servers
const resources = adapter.getResources();
for (const [server, serverResources] of resources) {
for (const resource of serverResources) {
console.log(`${server}: ${resource.uri} (${resource.mimeType})`);
}
}
Event Hooks
Observe all MCP activity:
const adapter = createMCPAdapter({
servers: [...],
// Hook into every MCP lifecycle event for logging or metrics
events: {
onConnect: (server) => console.log(`Connected: ${server}`),
onDisconnect: (server) => console.log(`Disconnected: ${server}`),
onToolCall: (server, tool, args) => console.log(`Calling: ${server}.${tool}`),
onToolResult: (server, tool, result) => console.log(`Result: ${server}.${tool}`),
onResourceUpdate: (server, uri, content) => console.log(`Resource updated: ${uri}`),
onError: (server, error) => console.error(`Error on ${server}:`, error),
},
});
Framework Integration
MCP is primarily server-side, but you can display tool status and approval requests through the orchestrator's .system bridge keys.
React
import { useAgentOrchestrator, useFact } from '@directive-run/react';
function MCPToolPanel() {
// Approval mode – tool calls require explicit sign-off
const orchestrator = useAgentOrchestrator({ runner, autoApproveToolCalls: false });
const { system } = orchestrator;
// Subscribe to agent status, pending approvals, and tool call history
const agent = useFact(system, '__agent');
const approval = useFact(system, '__approval');
const toolCalls = useFact(system, '__toolCalls');
return (
<div>
<p>Status: {agent?.status}</p>
<p>Pending approvals: {approval?.pending?.length ?? 0}</p>
<ul>
{toolCalls?.map((tc) => <li key={tc.id}>{tc.tool}: {tc.status}</li>)}
</ul>
</div>
);
}
Vue
<script setup>
import { createAgentOrchestrator } from '@directive-run/ai';
import { useFact } from '@directive-run/vue';
import { onUnmounted } from 'vue';
const orchestrator = createAgentOrchestrator({ runner, autoApproveToolCalls: false });
onUnmounted(() => orchestrator.dispose());
// Reactive refs for approval queue and tool call history
const approval = useFact(orchestrator.system, '__approval');
const toolCalls = useFact(orchestrator.system, '__toolCalls');
</script>
<template>
<p>Pending approvals: {{ approval?.pending?.length ?? 0 }}</p>
<ul>
<li v-for="tc in toolCalls" :key="tc.id">{{ tc.tool }}: {{ tc.status }}</li>
</ul>
</template>
Svelte
<script>
import { createAgentOrchestrator } from '@directive-run/ai';
import { useFact } from '@directive-run/svelte';
import { onDestroy } from 'svelte';
const orchestrator = createAgentOrchestrator({ runner, autoApproveToolCalls: false });
onDestroy(() => orchestrator.dispose());
// Svelte stores for approval queue and tool calls
const approval = useFact(orchestrator.system, '__approval');
const toolCalls = useFact(orchestrator.system, '__toolCalls');
</script>
<p>Pending approvals: {$approval?.pending?.length ?? 0}</p>
<ul>
{#each $toolCalls ?? [] as tc (tc.id)}
<li>{tc.tool}: {tc.status}</li>
{/each}
</ul>
Solid
import { createAgentOrchestrator } from '@directive-run/ai';
import { useFact } from '@directive-run/solid';
import { onCleanup, For } from 'solid-js';
function MCPToolPanel() {
const orchestrator = createAgentOrchestrator({ runner, autoApproveToolCalls: false });
onCleanup(() => orchestrator.dispose());
// Solid signals – call approval() and toolCalls() to read current values
const approval = useFact(orchestrator.system, '__approval');
const toolCalls = useFact(orchestrator.system, '__toolCalls');
return (
<div>
<p>Pending approvals: {approval()?.pending?.length ?? 0}</p>
<ul>
<For each={toolCalls() ?? []}>{(tc) => <li>{tc.tool}: {tc.status}</li>}</For>
</ul>
</div>
);
}
Lit
import { LitElement, html } from 'lit';
import { createAgentOrchestrator } from '@directive-run/ai';
import { FactController } from '@directive-run/lit';
class MCPToolPanel extends LitElement {
private orchestrator = createAgentOrchestrator({ runner, autoApproveToolCalls: false });
// Reactive controllers – trigger re-render when approval or tool state changes
private approval = new FactController(this, this.orchestrator.system, '__approval');
private toolCalls = new FactController(this, this.orchestrator.system, '__toolCalls');
disconnectedCallback() {
super.disconnectedCallback();
this.orchestrator.dispose();
}
render() {
return html`
<p>Pending approvals: ${this.approval.value?.pending?.length ?? 0}</p>
<ul>
${(this.toolCalls.value ?? []).map((tc) => html`<li>${tc.tool}: ${tc.status}</li>`)}
</ul>
`;
}
}
Next Steps
- Agent Orchestrator – AI agent orchestration
- Guardrails – Input/output validation
- Multi-Agent – Coordinating multiple agents

