Initial commit: Incus MCP Server implementation
- Complete MCP server for Incus container management - 10 tools for instance lifecycle operations (create, start, stop, etc.) - 2 resources for instance and remote server data - Support for multiple Incus remotes with TLS authentication - TypeScript implementation with comprehensive error handling - Test suite for validation and integration testing - MCP configuration for Claude Code integration 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
169
test/client.ts
Normal file
169
test/client.ts
Normal file
@@ -0,0 +1,169 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
||||
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
|
||||
import { spawn } from 'child_process';
|
||||
|
||||
class TestClient {
|
||||
private client: Client;
|
||||
private transport: StdioClientTransport;
|
||||
|
||||
constructor() {
|
||||
// Spawn the MCP server process
|
||||
const serverProcess = spawn('node', ['../build/index.js'], {
|
||||
stdio: ['pipe', 'pipe', 'inherit'],
|
||||
cwd: __dirname,
|
||||
});
|
||||
|
||||
this.transport = new StdioClientTransport({
|
||||
reader: serverProcess.stdout!,
|
||||
writer: serverProcess.stdin!,
|
||||
});
|
||||
|
||||
this.client = new Client(
|
||||
{
|
||||
name: 'incus-mcp-test-client',
|
||||
version: '0.1.0',
|
||||
},
|
||||
{
|
||||
capabilities: {},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
async connect() {
|
||||
await this.client.connect(this.transport);
|
||||
console.log('✅ Connected to Incus MCP server');
|
||||
}
|
||||
|
||||
async testListTools() {
|
||||
console.log('\n🔧 Testing list tools...');
|
||||
try {
|
||||
const result = await this.client.request(
|
||||
{ method: 'tools/list' },
|
||||
{ method: 'tools/list' }
|
||||
);
|
||||
console.log(`Found ${result.tools.length} tools:`);
|
||||
result.tools.forEach((tool: any) => {
|
||||
console.log(` - ${tool.name}: ${tool.description}`);
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('❌ Error listing tools:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async testListResources() {
|
||||
console.log('\n📚 Testing list resources...');
|
||||
try {
|
||||
const result = await this.client.request(
|
||||
{ method: 'resources/list' },
|
||||
{ method: 'resources/list' }
|
||||
);
|
||||
console.log(`Found ${result.resources.length} resources:`);
|
||||
result.resources.forEach((resource: any) => {
|
||||
console.log(` - ${resource.uri}: ${resource.name}`);
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('❌ Error listing resources:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async testIncusInfo() {
|
||||
console.log('\n💻 Testing incus info...');
|
||||
try {
|
||||
const result = await this.client.request(
|
||||
{
|
||||
method: 'tools/call',
|
||||
params: {
|
||||
name: 'incus_info',
|
||||
arguments: {},
|
||||
},
|
||||
},
|
||||
{
|
||||
method: 'tools/call',
|
||||
params: {
|
||||
name: 'incus_info',
|
||||
arguments: {},
|
||||
},
|
||||
}
|
||||
);
|
||||
console.log('Incus info result:', result);
|
||||
} catch (error) {
|
||||
console.error('❌ Error getting incus info:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async testListInstances() {
|
||||
console.log('\n📦 Testing list instances...');
|
||||
try {
|
||||
const result = await this.client.request(
|
||||
{
|
||||
method: 'tools/call',
|
||||
params: {
|
||||
name: 'incus_list_instances',
|
||||
arguments: {},
|
||||
},
|
||||
},
|
||||
{
|
||||
method: 'tools/call',
|
||||
params: {
|
||||
name: 'incus_list_instances',
|
||||
arguments: {},
|
||||
},
|
||||
}
|
||||
);
|
||||
console.log('Instances list result:', result);
|
||||
} catch (error) {
|
||||
console.error('❌ Error listing instances:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async testListRemotes() {
|
||||
console.log('\n🌐 Testing list remotes...');
|
||||
try {
|
||||
const result = await this.client.request(
|
||||
{
|
||||
method: 'tools/call',
|
||||
params: {
|
||||
name: 'incus_list_remotes',
|
||||
arguments: {},
|
||||
},
|
||||
},
|
||||
{
|
||||
method: 'tools/call',
|
||||
params: {
|
||||
name: 'incus_list_remotes',
|
||||
arguments: {},
|
||||
},
|
||||
}
|
||||
);
|
||||
console.log('Remotes list result:', result);
|
||||
} catch (error) {
|
||||
console.error('❌ Error listing remotes:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async runTests() {
|
||||
try {
|
||||
await this.connect();
|
||||
await this.testListTools();
|
||||
await this.testListResources();
|
||||
await this.testIncusInfo();
|
||||
await this.testListInstances();
|
||||
await this.testListRemotes();
|
||||
console.log('\n✅ All tests completed');
|
||||
} catch (error) {
|
||||
console.error('❌ Test failed:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Run tests
|
||||
async function main() {
|
||||
const testClient = new TestClient();
|
||||
await testClient.runTests();
|
||||
}
|
||||
|
||||
if (import.meta.url === `file://${process.argv[1]}`) {
|
||||
main().catch(console.error);
|
||||
}
|
||||
164
test/resource-test.js
Normal file
164
test/resource-test.js
Normal file
@@ -0,0 +1,164 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { spawn } from 'child_process';
|
||||
|
||||
async function testResources() {
|
||||
console.log('🔍 Testing MCP Resources...\n');
|
||||
|
||||
// Test reading instances list resource
|
||||
console.log('1. Testing incus://instances/list resource...');
|
||||
const instancesResourceMessage = {
|
||||
jsonrpc: '2.0',
|
||||
id: 1,
|
||||
method: 'resources/read',
|
||||
params: {
|
||||
uri: 'incus://instances/list'
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
const result = await sendMCPRequest(instancesResourceMessage);
|
||||
console.log(` ✅ Resource read successful`);
|
||||
if (result.contents && result.contents[0]) {
|
||||
const content = result.contents[0];
|
||||
console.log(` URI: ${content.uri}`);
|
||||
console.log(` MIME Type: ${content.mimeType}`);
|
||||
console.log(` Content length: ${content.text.length} chars`);
|
||||
|
||||
// Try to parse as JSON
|
||||
try {
|
||||
const instances = JSON.parse(content.text);
|
||||
console.log(` Found ${instances.length} instances in JSON format`);
|
||||
} catch (e) {
|
||||
console.log(` Content is not JSON: ${e.message}`);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(` ❌ Error: ${error.message}`);
|
||||
}
|
||||
|
||||
// Test reading remotes list resource
|
||||
console.log('\n2. Testing incus://remotes/list resource...');
|
||||
const remotesResourceMessage = {
|
||||
jsonrpc: '2.0',
|
||||
id: 2,
|
||||
method: 'resources/read',
|
||||
params: {
|
||||
uri: 'incus://remotes/list'
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
const result = await sendMCPRequest(remotesResourceMessage);
|
||||
console.log(` ✅ Resource read successful`);
|
||||
if (result.contents && result.contents[0]) {
|
||||
const content = result.contents[0];
|
||||
console.log(` URI: ${content.uri}`);
|
||||
console.log(` MIME Type: ${content.mimeType}`);
|
||||
console.log(` Content length: ${content.text.length} chars`);
|
||||
|
||||
// Try to parse as JSON
|
||||
try {
|
||||
const remotes = JSON.parse(content.text);
|
||||
const remoteNames = Object.keys(remotes);
|
||||
console.log(` Found ${remoteNames.length} remotes: ${remoteNames.join(', ')}`);
|
||||
} catch (e) {
|
||||
console.log(` Content is not JSON: ${e.message}`);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(` ❌ Error: ${error.message}`);
|
||||
}
|
||||
|
||||
// Test reading unknown resource
|
||||
console.log('\n3. Testing unknown resource...');
|
||||
const unknownResourceMessage = {
|
||||
jsonrpc: '2.0',
|
||||
id: 3,
|
||||
method: 'resources/read',
|
||||
params: {
|
||||
uri: 'incus://unknown/resource'
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
const result = await sendMCPRequest(unknownResourceMessage);
|
||||
console.log(` ❌ Unexpected success: ${JSON.stringify(result)}`);
|
||||
} catch (error) {
|
||||
console.log(` ✅ Expected error: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function sendMCPRequest(message) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const serverProcess = spawn('node', ['build/index.js'], {
|
||||
stdio: ['pipe', 'pipe', 'pipe']
|
||||
});
|
||||
|
||||
let response = '';
|
||||
let error = '';
|
||||
|
||||
serverProcess.stdout.on('data', (data) => {
|
||||
response += data.toString();
|
||||
});
|
||||
|
||||
serverProcess.stderr.on('data', (data) => {
|
||||
error += data.toString();
|
||||
});
|
||||
|
||||
serverProcess.on('close', (code) => {
|
||||
if (code !== 0) {
|
||||
reject(new Error(`Server exited with code ${code}: ${error}`));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Parse JSON-RPC response
|
||||
const lines = response.trim().split('\n');
|
||||
let jsonResponse = null;
|
||||
|
||||
for (const line of lines) {
|
||||
if (line.trim()) {
|
||||
try {
|
||||
const parsed = JSON.parse(line);
|
||||
if (parsed.id === message.id) {
|
||||
jsonResponse = parsed;
|
||||
break;
|
||||
}
|
||||
} catch (e) {
|
||||
// Skip non-JSON lines
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (jsonResponse) {
|
||||
if (jsonResponse.error) {
|
||||
reject(new Error(jsonResponse.error.message || 'Unknown error'));
|
||||
} else {
|
||||
resolve(jsonResponse.result);
|
||||
}
|
||||
} else {
|
||||
reject(new Error(`No valid response found in: ${response}`));
|
||||
}
|
||||
} catch (e) {
|
||||
reject(new Error(`Failed to parse response: ${e.message}\nResponse: ${response}`));
|
||||
}
|
||||
});
|
||||
|
||||
serverProcess.on('error', (err) => {
|
||||
reject(new Error(`Failed to start server: ${err.message}`));
|
||||
});
|
||||
|
||||
// Send the JSON-RPC message
|
||||
serverProcess.stdin.write(JSON.stringify(message) + '\n');
|
||||
serverProcess.stdin.end();
|
||||
});
|
||||
}
|
||||
|
||||
// Run resource tests
|
||||
testResources().then(() => {
|
||||
console.log('\n✅ All resource tests completed!');
|
||||
}).catch((error) => {
|
||||
console.error('\n❌ Resource test suite failed:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
187
test/simple-test.js
Normal file
187
test/simple-test.js
Normal file
@@ -0,0 +1,187 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { spawn } from 'child_process';
|
||||
import { promises as fs } from 'fs';
|
||||
|
||||
// Simple MCP client test
|
||||
async function testMCPServer() {
|
||||
console.log('🧪 Testing Incus MCP Server...\n');
|
||||
|
||||
// Test 1: List tools
|
||||
console.log('1. Testing list tools...');
|
||||
const listToolsMessage = {
|
||||
jsonrpc: '2.0',
|
||||
id: 1,
|
||||
method: 'tools/list',
|
||||
params: {}
|
||||
};
|
||||
|
||||
try {
|
||||
const toolsResult = await sendMCPRequest(listToolsMessage);
|
||||
console.log(` ✅ Found ${toolsResult.tools.length} tools`);
|
||||
toolsResult.tools.forEach(tool => {
|
||||
console.log(` - ${tool.name}`);
|
||||
});
|
||||
} catch (error) {
|
||||
console.log(` ❌ Error: ${error.message}`);
|
||||
}
|
||||
|
||||
// Test 2: List resources
|
||||
console.log('\n2. Testing list resources...');
|
||||
const listResourcesMessage = {
|
||||
jsonrpc: '2.0',
|
||||
id: 2,
|
||||
method: 'resources/list',
|
||||
params: {}
|
||||
};
|
||||
|
||||
try {
|
||||
const resourcesResult = await sendMCPRequest(listResourcesMessage);
|
||||
console.log(` ✅ Found ${resourcesResult.resources.length} resources`);
|
||||
resourcesResult.resources.forEach(resource => {
|
||||
console.log(` - ${resource.uri}: ${resource.name}`);
|
||||
});
|
||||
} catch (error) {
|
||||
console.log(` ❌ Error: ${error.message}`);
|
||||
}
|
||||
|
||||
// Test 3: Test incus info tool
|
||||
console.log('\n3. Testing incus_info tool...');
|
||||
const infoMessage = {
|
||||
jsonrpc: '2.0',
|
||||
id: 3,
|
||||
method: 'tools/call',
|
||||
params: {
|
||||
name: 'incus_info',
|
||||
arguments: {}
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
const infoResult = await sendMCPRequest(infoMessage);
|
||||
console.log(` ✅ Got incus info`);
|
||||
if (infoResult.content && infoResult.content[0]) {
|
||||
console.log(` First few chars: ${infoResult.content[0].text.substring(0, 100)}...`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(` ❌ Error: ${error.message}`);
|
||||
}
|
||||
|
||||
// Test 4: Test list instances
|
||||
console.log('\n4. Testing incus_list_instances tool...');
|
||||
const listInstancesMessage = {
|
||||
jsonrpc: '2.0',
|
||||
id: 4,
|
||||
method: 'tools/call',
|
||||
params: {
|
||||
name: 'incus_list_instances',
|
||||
arguments: {}
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
const instancesResult = await sendMCPRequest(listInstancesMessage);
|
||||
console.log(` ✅ Got instances list`);
|
||||
if (instancesResult.content && instancesResult.content[0]) {
|
||||
console.log(` Response: ${instancesResult.content[0].text.substring(0, 200)}...`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(` ❌ Error: ${error.message}`);
|
||||
}
|
||||
|
||||
// Test 5: Test list remotes
|
||||
console.log('\n5. Testing incus_list_remotes tool...');
|
||||
const listRemotesMessage = {
|
||||
jsonrpc: '2.0',
|
||||
id: 5,
|
||||
method: 'tools/call',
|
||||
params: {
|
||||
name: 'incus_list_remotes',
|
||||
arguments: {}
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
const remotesResult = await sendMCPRequest(listRemotesMessage);
|
||||
console.log(` ✅ Got remotes list`);
|
||||
if (remotesResult.content && remotesResult.content[0]) {
|
||||
console.log(` Response: ${remotesResult.content[0].text.substring(0, 200)}...`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(` ❌ Error: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function sendMCPRequest(message) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const serverProcess = spawn('node', ['build/index.js'], {
|
||||
stdio: ['pipe', 'pipe', 'pipe']
|
||||
});
|
||||
|
||||
let response = '';
|
||||
let error = '';
|
||||
|
||||
serverProcess.stdout.on('data', (data) => {
|
||||
response += data.toString();
|
||||
});
|
||||
|
||||
serverProcess.stderr.on('data', (data) => {
|
||||
error += data.toString();
|
||||
});
|
||||
|
||||
serverProcess.on('close', (code) => {
|
||||
if (code !== 0) {
|
||||
reject(new Error(`Server exited with code ${code}: ${error}`));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Parse JSON-RPC response
|
||||
const lines = response.trim().split('\n');
|
||||
let jsonResponse = null;
|
||||
|
||||
for (const line of lines) {
|
||||
if (line.trim()) {
|
||||
try {
|
||||
const parsed = JSON.parse(line);
|
||||
if (parsed.id === message.id) {
|
||||
jsonResponse = parsed;
|
||||
break;
|
||||
}
|
||||
} catch (e) {
|
||||
// Skip non-JSON lines
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (jsonResponse) {
|
||||
if (jsonResponse.error) {
|
||||
reject(new Error(jsonResponse.error.message || 'Unknown error'));
|
||||
} else {
|
||||
resolve(jsonResponse.result);
|
||||
}
|
||||
} else {
|
||||
reject(new Error(`No valid response found in: ${response}`));
|
||||
}
|
||||
} catch (e) {
|
||||
reject(new Error(`Failed to parse response: ${e.message}\nResponse: ${response}`));
|
||||
}
|
||||
});
|
||||
|
||||
serverProcess.on('error', (err) => {
|
||||
reject(new Error(`Failed to start server: ${err.message}`));
|
||||
});
|
||||
|
||||
// Send the JSON-RPC message
|
||||
serverProcess.stdin.write(JSON.stringify(message) + '\n');
|
||||
serverProcess.stdin.end();
|
||||
});
|
||||
}
|
||||
|
||||
// Run tests
|
||||
testMCPServer().then(() => {
|
||||
console.log('\n✅ All tests completed!');
|
||||
}).catch((error) => {
|
||||
console.error('\n❌ Test suite failed:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
Reference in New Issue
Block a user