Create an AI Agent
Build an AI Agent that runs on-device with Model Context Protocol (MCP) tools. This tutorial uses create-mim to scaffold a complete AI Agent project.
If you haven't already, read How AI Agent mims Work first to understand the architecture and core concepts. This tutorial puts those concepts into practice.
What You'll Build
An AI Agent mim that:
- Exposes an MCP server with custom tools
- Uses
@mimik/agent-kitfor AI orchestration - Streams responses via Server-Sent Events (SSE)
- Discovers nearby devices using mimOE's mesh capabilities
Prerequisites
Before starting, ensure you have:
- Node.js 18+ installed
- mimOE running with the AI Foundation Addon (see Quick Start)
- A model with tool use support loaded for inference (see below)
Verify mimOE is running:
curl http://localhost:8083/mimik-ai/openai/v1/models \
-H "Authorization: Bearer 1234"
Model Requirements for Function Calling
Not all models support function calling (tool use). The model must be specifically trained for tool use capabilities. Models without this training cannot be used for AI Agents that need to call MCP tools.
Recommended model: Qwen3 (e.g., qwen3-1.7b). It provides both reasoning and tool use support, optimized for on-device inference.
Context Size Requirements
Tool use requires a larger context window because the conversation includes tool definitions, tool calls, and tool results. Two settings control this:
-
initContextSize(Model Registry): Total context window size (input + output combined). Set when uploading the model:{
"initContextSize": 12000
}For tool-using agents, 12K+ tokens is recommended. See Model Registry API.
-
max_tokens(Inference/agent-kit): Maximum tokens to generate in the response. Set in your agent configuration:llm: {
max_tokens: 4096, // Maximum output tokens
// ... other config
}This limits output length, not total context. See Inference API.
initContextSize is the total budget. Your input (instructions + tool definitions + history) uses part of it, leaving the rest for output. If your input uses 8K tokens and initContextSize is 12K, you have 4K tokens available for the response.
Step 1: Create the Project
Use create-mim to scaffold an AI Agent project:
npx create-mim create my-agent -t ai-agent
This creates a my-agent/ directory with the complete project structure.
Navigate to the project and install dependencies:
cd my-agent
npm install
Step 2: Project Structure
The generated project contains:
my-agent/
├── src/
│ ├── index.js # Router with /mcp, /healthcheck, /chat endpoints
│ ├── agent.js # AI Agent orchestration
│ └── tools.js # MCP tools definition
├── config/
│ └── default.yml # Package configuration
├── test/
│ └── simple.http # REST client tests
├── package.json
├── webpack.config.js
└── eslint.config.js
Step 3: Understanding the Code
Entry Point (src/index.js)
The entry point sets up three endpoints with SSE streaming support:
const Router = require('router');
const { tools, getMcpEndpoints } = require('./tools');
const agent = require('./agent');
let isHeaderSet = false;
const sendSSE = (res, data) => {
if (!isHeaderSet) {
res.statusCode = 200;
res.setHeader('content-type', 'text/event-stream');
res.setHeader('transfer-encoding', 'chunked');
isHeaderSet = true;
}
const message = `data: ${JSON.stringify(data)}\n\n`;
res.write(message);
};
const app = Router();
// MCP protocol handler
app.post('/mcp', (req, res) => {
tools.handleMcpRequest(req.body).then((response) => {
if (response) {
res.end(JSON.stringify(response));
} else {
res.statusCode = 204;
res.end();
}
}).catch((error) => {
res.statusCode = 500;
res.end(JSON.stringify({ error }));
});
});
// Health check
app.get('/healthcheck', (req, res) => {
const { info } = global.context;
res.end(JSON.stringify({ status: 'ok', info }));
});
// Chat endpoint with SSE streaming
app.post('/chat', async (req, res) => {
try {
const result = await agent.run(getMcpEndpoints());
for await (const event of result) {
sendSSE(res, event);
}
res.end();
} catch (error) {
console.error('Agent error:', error);
if (isHeaderSet) {
sendSSE(res, { error: { message: error.message, type: 'agent_error' } });
res.end();
} else {
res.end(JSON.stringify({ error: { message: error.message, type: 'agent_error' } }));
}
}
});
mimikModule.exports = (context, req, res) => {
global.context = context;
global.http = global.context.http;
app(req, res, (e) => {
res.end(JSON.stringify({ code: e ? 400 : 404, message: e || 'Not Found' }));
});
};
Key points:
mimikModule.exportsis the mim entry point (notmodule.exports)- The
contextobject provides runtime APIs likecontext.httpandcontext.info sendSSEhelper handles SSE formatting with proper headers- Error handling for both streaming and non-streaming error responses
MCP Tools (src/tools.js)
agent-kit only accepts tool definitions via the MCP protocol. Even for tools defined in the same mim, we use mcp-kit to create an MCP server endpoint. This design provides:
- Observability: All tool calls go through the API gateway and are logged automatically
- Reusability: The same MCP server can serve multiple agents
- Testability: Tools can be tested independently via HTTP
- Standard protocol: MCP is portable across different agent frameworks
Define tools the AI Agent can use with @mimik/mcp-kit:
const { McpServer, z } = require('@mimik/mcp-kit');
const server = new McpServer({
name: 'minsight MCP server',
version: '1.0.0',
});
// HTTP request helper using context.http
function requestAction(opt) {
return new Promise((resolve, reject) => {
global.http.request({
url: opt.url,
type: opt.method,
headers: opt.headers,
authorization: opt.authorization,
data: opt.jsonBody && JSON.stringify(opt.jsonBody),
success: (result) => resolve(result),
error: (err) => {
if (err instanceof Error) {
reject(err);
} else {
reject(new Error(err.content || err.message));
}
},
});
});
}
// Simple tool: add two numbers
server.tool('add', 'Add two numbers', { a: z.number(), b: z.number() }, (args) => Promise.resolve({
content: [{ type: 'text', text: String(args.a + args.b) }],
}));
// Discovery tool: find nearby nodes
server.tool('discoverLocal', 'Discover nodes around me', {}, async () => {
const { httpPort } = global.context.info;
const { INSIGHT_API_KEY } = global.context.env;
try {
const res = await requestAction({
url: `http://127.0.0.1:${httpPort}/mimik-mesh/insight/v1/nodes?type=linkLocal`,
headers: { 'Authorization': `Bearer ${INSIGHT_API_KEY}` },
});
return {
content: [{ type: 'text', text: res.data }],
};
} catch (err) {
return {
content: [{ type: 'text', text: err.message }],
};
}
});
// Returns MCP endpoints for the agent to connect to
function getMcpEndpoints() {
const { info } = global.context;
const { httpPort, apiRoot } = info;
const mcpEndpoints = [{
name: 'minsight mcp',
url: `http://127.0.0.1:${httpPort}${apiRoot}/mcp`,
}];
return { mcpEndpoints };
}
module.exports = {
tools: server,
getMcpEndpoints,
};
Key components:
- McpServer: Creates an MCP server that handles JSON-RPC protocol
- requestAction: Helper to make HTTP requests using
context.http - server.tool(): Registers tools with name, description, schema, and handler
- getMcpEndpoints(): Returns endpoints the agent will connect to
Schema Helpers
The z object provides schema validation:
// Basic types
z.string() // String type
z.number() // Number type
z.boolean() // Boolean type
// Complex types
z.enum(['a', 'b', 'c']) // Enumeration
z.array(z.string()) // Array of strings
z.object({ key: z.string() }) // Nested object
z.optional(z.string()) // Optional field
// Modifiers
.describe('Description') // Add description
.default(value) // Set default value (makes field optional)
Agent Orchestration (src/agent.js)
The agent uses @mimik/agent-kit to orchestrate AI with tools:
const { Agent } = require('@mimik/agent-kit');
const instructions = 'You are a helpful assistant with access to MCP tools. Use them to answer user questions.';
async function* run(opt) {
const { httpPort } = global.context.info;
const { INFERENCE_API_KEY } = global.context.env;
const agent = new Agent({
name: 'MCP Assistant',
instructions,
httpClient: global.http,
mcpEndpoints: opt?.mcpEndpoints,
llm: {
endpoint: `http://127.0.0.1:${httpPort}/mimik-ai/openai/v1/chat/completions`,
apiKey: `Bearer ${INFERENCE_API_KEY}`,
}
});
const userMessage = 'find devices around me'; // Add /no_think suffix for Qwen3
const result = await agent.run(userMessage, {
toolApproval: async (toolCalls) => ({
stopAfterExecution: false, // Default: false (continue conversation)
approvals: toolCalls.map(() => true),
}),
});
for await (const event of result) {
// Forward relevant events to client
if (event.type === 'raw_model_stream_event') {
yield event.data.event;
} else if (event.type === 'tool_calls_detected') {
yield {
type: 'tool_calls_start',
tool_calls: event.data.toolCalls,
};
} else if (event.type === 'tool_results') {
yield {
type: 'tool_calls_complete',
results: event.data.results,
};
} else if (event.type === 'conversation_complete') {
yield {
type: 'done',
final_output: event.data.finalOutput,
};
break;
}
}
}
module.exports = { run };
The agent:
- Connects to MCP endpoints to discover available tools
- Sends user messages to the AI model
- Automatically calls tools when the AI decides to use them
- Streams events back to the client in real-time
The /no_think directive is specific to Qwen3 models. Qwen3 supports a "thinking" mode where it reasons through problems step-by-step. Appending /no_think to prompts disables this for faster, more direct responses. This is recommended for tool-use scenarios where you want the model to act quickly. If using other models, remove the /no_think suffix.
Streaming Events
The agent emits these event types:
| Event Type | Description |
|---|---|
raw_model_stream_event | Raw streaming tokens from the model |
tool_calls_detected | Tools the model wants to call |
tool_results | Results from tool execution |
conversation_complete | Final result with complete output |
Advanced Configuration
For more control, you can configure the LLM settings:
const { httpPort } = global.context.info;
const { INFERENCE_API_KEY } = global.context.env;
const agent = new Agent({
name: 'MCP Assistant',
instructions,
httpClient: global.http,
mcpEndpoints: opt?.mcpEndpoints,
maxIterations: 5, // Max conversation loops (default: 5)
llm: {
endpoint: `http://127.0.0.1:${httpPort}/mimik-ai/openai/v1/chat/completions`,
apiKey: `Bearer ${INFERENCE_API_KEY}`,
model: 'qwen3-1.7b', // Qwen3 recommended for tool use
temperature: 0.1, // Lower = more deterministic
max_tokens: 2048, // Maximum response length
no_think: true // Qwen3: disable thinking mode for faster responses
}
});
// Optionally initialize explicitly to preload tools
await agent.initialize();
Use context.info.httpPort to get the port dynamically instead of hardcoding 8083. Use separate environment variables for each API:
INFERENCE_API_KEY: For AI Foundation inference APIINSIGHT_API_KEY: For Mesh Foundation discovery API
Step 4: Build the Project
Build the mim using webpack:
npm run build
This transpiles ES6+ to ES5 (required for the Duktape runtime) and bundles everything into build/index.js.
Package as a deployable tar:
npm run package
This creates deploy/my-agent-1.0.0.tar.
Step 5: Deploy to mimOE
Deploy using the MCM API. First, get your MCM API key:
cat mimoe-api-key.env
Upload the image:
curl -X POST http://localhost:8083/mcm/v1/images \
-H "Authorization: Bearer $MCM_API_KEY" \
-F "image=@deploy/my-agent-1.0.0.tar"
Deploy the mim:
curl -X POST http://localhost:8083/mcm/v1/mims \
-H "Authorization: Bearer $MCM_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "my-agent",
"image": "my-agent",
"env": {
"MCM.BASE_API_PATH": "/my-agent/v1",
"MCM.API_ALIAS": "true",
"INFERENCE_API_KEY": "1234",
"INSIGHT_API_KEY": "1234"
}
}'
Step 6: Test the AI Agent
Health Check
curl http://localhost:8083/api/my-agent/v1/healthcheck
Expected response:
{
"status": "ok",
"info": {
"httpPort": 8083,
"apiRoot": "/api/my-agent/v1"
}
}
List MCP Tools
Initialize the MCP connection and list available tools:
# Initialize
curl -X POST http://localhost:8083/api/my-agent/v1/mcp \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": "init-1",
"method": "initialize",
"params": {
"protocolVersion": "2025-03-26",
"capabilities": { "tools": {} },
"clientInfo": { "name": "test-client", "version": "1.0.0" }
}
}'
# List tools
curl -X POST http://localhost:8083/api/my-agent/v1/mcp \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": "list-1",
"method": "tools/list"
}'
Call a Tool Directly
curl -X POST http://localhost:8083/api/my-agent/v1/mcp \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": "call-1",
"method": "tools/call",
"params": {
"name": "add",
"arguments": { "a": 5, "b": 3 }
}
}'
Expected response:
{
"jsonrpc": "2.0",
"id": "call-1",
"result": {
"content": [{ "type": "text", "text": "8" }]
}
}
Chat with the Agent
The /chat endpoint streams SSE events:
curl -X POST http://localhost:8083/api/my-agent/v1/chat \
-H "Content-Type: application/json" \
-d '{}'
You'll receive streamed events as the agent thinks, calls tools, and generates responses.
Step 7: Add Custom Tools
Extend the agent by adding your own tools in src/tools.js:
// Tool with optional parameters and defaults
server.tool('search', 'Search for items in the database', {
query: z.string().describe('Search query'),
limit: z.number().default(10).describe('Maximum results to return'),
category: z.optional(z.string()).describe('Filter by category')
}, async (args) => {
const { query, limit, category } = args;
// Implementation here
return {
content: [{ type: 'text', text: `Found results for "${query}"` }],
};
});
// Tool with enum options
server.tool('setOrderType', 'Set the order type', {
type: z.enum(['pickup', 'delivery', 'dine-in']).default('pickup').describe('Order type'),
priority: z.enum(['low', 'normal', 'high']).default('normal').describe('Priority level')
}, async (args) => {
return {
content: [{ type: 'text', text: `Order set to ${args.type} (${args.priority} priority)` }],
};
});
// Tool using persistent storage
server.tool('saveNote', 'Save a note to persistent storage', {
key: z.string().describe('Note identifier'),
value: z.string().describe('Note content')
}, async (args) => {
await global.context.storage.setItem(args.key, args.value);
return {
content: [{ type: 'text', text: `Saved note "${args.key}"` }],
};
});
// Tool that calls an external API
server.tool('getWeather', 'Get weather for a city', {
city: z.string().describe('City name')
}, async (args) => {
try {
const res = await requestAction({
url: `https://api.weather.example/v1/current?city=${args.city}`,
});
return { content: [{ type: 'text', text: res.data }] };
} catch (err) {
return { content: [{ type: 'text', text: `Error: ${err.message}` }] };
}
});
After adding tools, rebuild and redeploy:
npm run build && npm run package
# Then redeploy via MCM API
Step 8: Advanced Security (Optional)
The generated template approves all tool calls. For production, you can add security controls.
Tool Whitelisting
Control which tools are available from each MCP server:
const agent = new Agent({
name: 'Secure Assistant',
instructions: 'Help users with controlled tool access.',
httpClient: global.http,
mcpEndpoints: [
// Simple endpoint - all tools allowed
'http://localhost:3000/mcp',
// Only allow specific tools (include mode)
{
url: 'http://localhost:3001/mcp',
options: {
toolWhitelist: ['readFile', 'listDirectory'],
whitelistMode: 'include'
}
},
// Block dangerous tools (exclude mode)
{
url: 'http://localhost:3002/mcp',
options: {
toolWhitelist: ['deleteFile', 'formatDisk'],
whitelistMode: 'exclude'
}
}
]
});
Runtime Tool Approval
Add dynamic approval logic in the toolApproval callback:
const result = await agent.run(userMessage, {
toolApproval: async (toolCalls) => ({
stopAfterExecution: false,
approvals: toolCalls.map(tool => {
// Block destructive operations
if (tool.function.name.includes('delete')) {
return { approve: false, reason: 'Destructive operations not allowed' };
}
// Approve everything else
return true;
})
})
});
For tools requiring human approval, implement a multi-step flow in your API handler:
- First request: Return
{ requiresApproval: true, pendingTools: toolCalls }when sensitive tools are detected - UI prompts user: Client shows the pending tools and asks for confirmation
- Second request: Client re-sends with
{ approvedTools: [...] } - Re-run with approvals: Agent runs again,
toolApprovalcallback checks against the pre-approved list
This pattern keeps humans in control of sensitive operations while maintaining the agentic flow.
Recap: The Four Things You Implemented
This tutorial walked you through implementing the four pieces of an AI Agent mim (as explained in How AI Agent mims Work):
| Config | File | What You Did |
|---|---|---|
| API Handler | src/index.js | Set up /chat, /mcp, /healthcheck endpoints with SSE streaming |
| Instructions | src/agent.js | Defined agent behavior in natural language |
| MCP Endpoints | src/tools.js | Created tools with @mimik/mcp-kit and exposed via /mcp |
| Context Retrieval | (optional) | Use mkb or edis for RAG and secured data (not covered in this basic tutorial) |
Serverless Execution
Mims are stateless. Each request instantiates a fresh execution context. Use context.storage for persistence between requests.
Troubleshooting
Build Fails with Syntax Errors
The Duktape runtime only supports ES5. Ensure webpack is transpiling correctly:
# Check webpack.config.js targets ES5
npm run build -- --mode development
Tool Not Found
Ensure the tool is registered before the mim is packaged:
- Check
tools.jsexports the server - Rebuild:
npm run build && npm run package - Redeploy the mim
Agent Returns Empty Response
Check that:
- A model is loaded in mimOE (
GET /mimik-ai/openai/v1/models) - The
httpClientis passed correctly to the Agent (global.http) - MCP endpoints are correctly configured in
getMcpEndpoints() - The mim is deployed with correct environment variables (
INFERENCE_API_KEY,INSIGHT_API_KEY)
Next Steps
- Agent Kit API Reference: Agent configuration options
- Context API: Available runtime APIs
- Mesh Discovery: Connect agents across devices