Complete guide to Lynkr's tool calling system, execution modes, and custom tool development.
Lynkr supports two tool execution modes:
- Server Mode (default): Tools execute on Lynkr server
- Client Mode (passthrough): Tools execute on client (Claude Code CLI/Cursor)
This enables flexible deployment scenarios: centralized tooling, security policies, or client-side file access.
How it works:
- Client sends request without tools
- Lynkr injects standard tools
- Model requests tool execution
- Tools run on Lynkr server
- Results sent back to model
Benefits:
- ✅ Centralized control
- ✅ Policy enforcement
- ✅ Consistent environment
- ✅ Works with any client
Use cases:
- Production deployments
- Team environments
- Policy-enforced workflows
- Air-gapped deployments
Configuration:
# Default - no configuration needed
# Server mode activates when client doesn't send toolsHow it works:
- Client sends request with tools
- Lynkr passes tools to model
- Model requests tool execution
- Client executes tools locally
- Results sent back through Lynkr
Benefits:
- ✅ Local file system access
- ✅ User-specific permissions
- ✅ No server-side execution
- ✅ Familiar CLI behavior
Use cases:
- Claude Code CLI (default behavior)
- Local development
- Personal use
- Custom tooling
Configuration:
# Client sends tools in request
# Lynkr automatically uses passthrough modePurpose: Read file contents
Parameters:
file_path(string, required): Absolute path to fileoffset(number, optional): Line number to start reading fromlimit(number, optional): Number of lines to read
Example:
{
"name": "Read",
"input": {
"file_path": "/path/to/file.js",
"offset": 0,
"limit": 100
}
}Features:
- Automatic truncation (2,000 lines max)
- Line numbering
- UTF-8 support
Purpose: Write content to file
Parameters:
file_path(string, required): Absolute path to filecontent(string, required): File content
Example:
{
"name": "Write",
"input": {
"file_path": "/path/to/file.js",
"content": "console.log('Hello');"
}
}Features:
- Creates directories if needed
- Overwrites existing files
- UTF-8 encoding
Purpose: Find and replace in file
Parameters:
file_path(string, required): Absolute path to fileold_string(string, required): Text to findnew_string(string, required): Replacement textreplace_all(boolean, optional): Replace all occurrences
Example:
{
"name": "Edit",
"input": {
"file_path": "/path/to/file.js",
"old_string": "var x = 1;",
"new_string": "const x = 1;",
"replace_all": true
}
}Purpose: Check git repository status
Example:
{
"name": "git_status",
"input": {}
}Returns:
- Untracked files
- Modified files
- Staged changes
- Current branch
Purpose: Show git diff
Parameters:
staged(boolean, optional): Show staged changes only
Example:
{
"name": "git_diff",
"input": {
"staged": false
}
}Purpose: Create git commit
Parameters:
message(string, required): Commit messagefiles(array, optional): Files to stage and commit
Example:
{
"name": "git_commit",
"input": {
"message": "Fix authentication bug",
"files": ["src/auth.js", "test/auth.test.js"]
}
}Policy enforcement:
# Prevent git push
POLICY_GIT_ALLOW_PUSH=false
# Require tests before commit
POLICY_GIT_REQUIRE_TESTS=true
POLICY_GIT_TEST_COMMAND="npm test"Purpose: Execute shell commands
Parameters:
command(string, required): Shell command to executetimeout(number, optional): Timeout in milliseconds (default: 120000)
Example:
{
"name": "Bash",
"input": {
"command": "npm test",
"timeout": 60000
}
}Features:
- Automatic truncation (1,000 lines max)
- Working directory preservation
- Environment variable access
- Timeout protection
Security:
# Commands are NOT sandboxed by default
# Use with caution in server modePurpose: Search file contents (ripgrep-powered)
Parameters:
pattern(string, required): Regex pattern to searchpath(string, optional): Directory to search (default: workspace)glob(string, optional): File pattern filter (e.g., "*.js")type(string, optional): File type (e.g., "js", "py")output_mode(string, optional): "content", "files_with_matches", "count"
Example:
{
"name": "Grep",
"input": {
"pattern": "function.*Auth",
"glob": "*.js",
"output_mode": "content"
}
}Purpose: Find files by pattern
Parameters:
pattern(string, required): Glob pattern (e.g., "**/*.js")path(string, optional): Directory to search
Example:
{
"name": "Glob",
"input": {
"pattern": "src/**/*.test.js"
}
}Purpose: Search long-term memories
Parameters:
query(string, required): Search query
Example:
{
"name": "memory_search",
"input": {
"query": "authentication preferences"
}
}Returns:
- Top N relevant memories (default: 5)
- Memory type, content, importance
- Creation and access timestamps
Purpose: Manually add memory
Parameters:
content(string, required): Memory contentmemory_type(string, required): "preference", "decision", "fact", "entity", "relationship"importance(number, optional): 0.0-1.0 (default: 1.0)
Example:
{
"name": "memory_add",
"input": {
"content": "User prefers TypeScript over JavaScript",
"memory_type": "preference",
"importance": 0.9
}
}Purpose: Delete specific memory
Parameters:
query(string, required): Search query to find memory to delete
Example:
{
"name": "memory_forget",
"input": {
"query": "old preference about MongoDB"
}
}Lynkr supports MCP for dynamic tool registration.
Features:
- Automatic MCP server discovery
- JSON-RPC 2.0 communication
- Dynamic tool registration
- Optional sandbox isolation
Enable MCP:
MCP_ENABLED=true # default: trueSandbox mode:
# Enable Docker sandbox for MCP tools
MCP_SANDBOX_ENABLED=true # default: true
# Docker image for sandbox
MCP_SANDBOX_IMAGE=ubuntu:22.04Locations searched:
./mcp-servers/(workspace directory)~/.mcp/servers/(user directory)- Environment variable:
MCP_SERVER_PATH
Example MCP server:
{
"name": "my-custom-tool",
"description": "Does something useful",
"inputSchema": {
"type": "object",
"properties": {
"input": {
"type": "string",
"description": "Input parameter"
}
},
"required": ["input"]
}
}MCP tools are automatically registered and available to models:
{
"name": "my-custom-tool",
"input": {
"input": "test value"
}
}File structure:
src/tools/
├── workspace.js (Read, Write, Edit)
├── git.js (git_status, git_diff, git_commit)
├── bash.js (Bash)
├── search.js (Grep, Glob)
├── memory.js (memory_search, memory_add, memory_forget)
└── custom.js (Your custom tools)
Custom tool template:
// src/tools/custom.js
const logger = require("pino")();
/**
* Custom tool definition
*/
const myCustomTool = {
name: "my_custom_tool",
description: "Does something useful",
input_schema: {
type: "object",
properties: {
input: {
type: "string",
description: "Input parameter"
}
},
required: ["input"]
}
};
/**
* Custom tool implementation
*/
async function executeMyCustomTool(input) {
try {
logger.info({ input }, "Executing my_custom_tool");
// Your tool logic here
const result = doSomething(input);
return {
success: true,
result: result
};
} catch (error) {
logger.error({ error }, "my_custom_tool failed");
throw error;
}
}
module.exports = {
myCustomTool,
executeMyCustomTool
};File: src/tools/index.js
const { myCustomTool, executeMyCustomTool } = require("./custom");
// Add to STANDARD_TOOLS
const STANDARD_TOOLS = [
// ... existing tools
myCustomTool
];
// Add to tool executor
async function executeTool(toolName, toolInput) {
switch (toolName) {
// ... existing cases
case "my_custom_tool":
return await executeMyCustomTool(toolInput);
default:
throw new Error(`Unknown tool: ${toolName}`);
}
}- Error handling:
try {
// Tool logic
} catch (error) {
logger.error({ error, input }, "Tool execution failed");
throw new Error(`Tool failed: ${error.message}`);
}- Input validation:
function validateInput(input) {
if (!input || typeof input !== "string") {
throw new Error("Invalid input: expected string");
}
}- Logging:
logger.info({
toolName: "my_custom_tool",
input: input,
duration: Date.now() - startTime
}, "Tool executed successfully");- Timeout protection:
const timeout = 30000; // 30 seconds
const result = await Promise.race([
executeTool(input),
new Promise((_, reject) =>
setTimeout(() => reject(new Error("Tool timeout")), timeout)
)
]);Prevent git push:
POLICY_GIT_ALLOW_PUSH=falseRequire tests before commit:
POLICY_GIT_REQUIRE_TESTS=true
POLICY_GIT_TEST_COMMAND="npm test"Example:
User: "Commit these changes"
Assistant: *Runs git commit*
Lynkr: [Blocks] Running tests first (POLICY_GIT_REQUIRE_TESTS=true)
Lynkr: *Executes npm test*
Lynkr: Tests passed, proceeding with commit
Restrict allowed hosts:
WEB_SEARCH_ALLOWED_HOSTS=github.com,stackoverflow.comCustom search endpoint:
WEB_SEARCH_ENDPOINT=http://localhost:8888/searchRestrict workspace access:
WORKSPACE_ROOT=/path/to/projectsMax agent loop iterations:
POLICY_MAX_STEPS=8Risks:
- Tools run with Lynkr server permissions
- Can access server filesystem
- Can execute arbitrary commands
Mitigations:
- Run as unprivileged user:
# Create dedicated user
useradd -r -s /bin/false lynkr
# Run as lynkr user
sudo -u lynkr npm start- Use Docker isolation:
# docker-compose.yml
services:
lynkr:
user: "1000:1000" # Non-root user
read_only: true # Read-only root filesystem
volumes:
- ./workspace:/workspace # Limited access- Enable policies:
POLICY_GIT_ALLOW_PUSH=false
POLICY_GIT_REQUIRE_TESTS=true
WEB_SEARCH_ALLOWED_HOSTS=github.com
WORKSPACE_ROOT=/workspaceRisks:
- Tools run with client user permissions
- Can access client filesystem
- Can execute commands on client machine
Mitigations:
- Review tool calls before execution
- Use Claude Code CLI safety features
- Run client in restricted environment
LOG_LEVEL=debug npm startOutput:
{
"level": "debug",
"msg": "Tool executed",
"toolName": "Read",
"input": {"file_path": "/path/to/file.js"},
"duration": 12,
"success": true
}# Test Read tool
curl -X POST http://localhost:8081/v1/messages \
-H "Content-Type: application/json" \
-d '{
"model": "claude-3.5-sonnet",
"messages": [{"role": "user", "content": "Read package.json"}],
"max_tokens": 1024
}'- MCP Integration Guide - Model Context Protocol setup
- Production Guide - Production deployment
- API Reference - API endpoints
- FAQ - Common questions
- GitHub Discussions - Ask questions
- GitHub Issues - Report issues