@ironclads/incus-mcp 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/settings.local.json +10 -0
- package/.eslintrc.json +23 -0
- package/.mcp.json +38 -0
- package/.prettierrc +8 -0
- package/CLAUDE.md +127 -0
- package/README.md +315 -0
- package/build/incus.d.ts +17 -0
- package/build/incus.d.ts.map +1 -0
- package/build/incus.js +66 -0
- package/build/incus.js.map +1 -0
- package/build/index.d.ts +3 -0
- package/build/index.d.ts.map +1 -0
- package/build/index.js +291 -0
- package/build/index.js.map +1 -0
- package/build/schemas.d.ts +4 -0
- package/build/schemas.d.ts.map +1 -0
- package/build/schemas.js +202 -0
- package/build/schemas.js.map +1 -0
- package/mcp-config-example.json +11 -0
- package/package.json +45 -0
- package/src/incus.ts +71 -0
- package/src/index.ts +340 -0
- package/src/schemas.ts +204 -0
- package/test/client.ts +169 -0
- package/test/resource-test.js +164 -0
- package/test/simple-test.js +187 -0
- package/tsconfig.json +20 -0
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { spawn } from 'child_process';
|
|
4
|
+
|
|
5
|
+
async function testResources() {
|
|
6
|
+
console.log('š Testing MCP Resources...\n');
|
|
7
|
+
|
|
8
|
+
// Test reading instances list resource
|
|
9
|
+
console.log('1. Testing incus://instances/list resource...');
|
|
10
|
+
const instancesResourceMessage = {
|
|
11
|
+
jsonrpc: '2.0',
|
|
12
|
+
id: 1,
|
|
13
|
+
method: 'resources/read',
|
|
14
|
+
params: {
|
|
15
|
+
uri: 'incus://instances/list'
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
const result = await sendMCPRequest(instancesResourceMessage);
|
|
21
|
+
console.log(` ā
Resource read successful`);
|
|
22
|
+
if (result.contents && result.contents[0]) {
|
|
23
|
+
const content = result.contents[0];
|
|
24
|
+
console.log(` URI: ${content.uri}`);
|
|
25
|
+
console.log(` MIME Type: ${content.mimeType}`);
|
|
26
|
+
console.log(` Content length: ${content.text.length} chars`);
|
|
27
|
+
|
|
28
|
+
// Try to parse as JSON
|
|
29
|
+
try {
|
|
30
|
+
const instances = JSON.parse(content.text);
|
|
31
|
+
console.log(` Found ${instances.length} instances in JSON format`);
|
|
32
|
+
} catch (e) {
|
|
33
|
+
console.log(` Content is not JSON: ${e.message}`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
} catch (error) {
|
|
37
|
+
console.log(` ā Error: ${error.message}`);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Test reading remotes list resource
|
|
41
|
+
console.log('\n2. Testing incus://remotes/list resource...');
|
|
42
|
+
const remotesResourceMessage = {
|
|
43
|
+
jsonrpc: '2.0',
|
|
44
|
+
id: 2,
|
|
45
|
+
method: 'resources/read',
|
|
46
|
+
params: {
|
|
47
|
+
uri: 'incus://remotes/list'
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
const result = await sendMCPRequest(remotesResourceMessage);
|
|
53
|
+
console.log(` ā
Resource read successful`);
|
|
54
|
+
if (result.contents && result.contents[0]) {
|
|
55
|
+
const content = result.contents[0];
|
|
56
|
+
console.log(` URI: ${content.uri}`);
|
|
57
|
+
console.log(` MIME Type: ${content.mimeType}`);
|
|
58
|
+
console.log(` Content length: ${content.text.length} chars`);
|
|
59
|
+
|
|
60
|
+
// Try to parse as JSON
|
|
61
|
+
try {
|
|
62
|
+
const remotes = JSON.parse(content.text);
|
|
63
|
+
const remoteNames = Object.keys(remotes);
|
|
64
|
+
console.log(` Found ${remoteNames.length} remotes: ${remoteNames.join(', ')}`);
|
|
65
|
+
} catch (e) {
|
|
66
|
+
console.log(` Content is not JSON: ${e.message}`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
} catch (error) {
|
|
70
|
+
console.log(` ā Error: ${error.message}`);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Test reading unknown resource
|
|
74
|
+
console.log('\n3. Testing unknown resource...');
|
|
75
|
+
const unknownResourceMessage = {
|
|
76
|
+
jsonrpc: '2.0',
|
|
77
|
+
id: 3,
|
|
78
|
+
method: 'resources/read',
|
|
79
|
+
params: {
|
|
80
|
+
uri: 'incus://unknown/resource'
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
try {
|
|
85
|
+
const result = await sendMCPRequest(unknownResourceMessage);
|
|
86
|
+
console.log(` ā Unexpected success: ${JSON.stringify(result)}`);
|
|
87
|
+
} catch (error) {
|
|
88
|
+
console.log(` ā
Expected error: ${error.message}`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async function sendMCPRequest(message) {
|
|
93
|
+
return new Promise((resolve, reject) => {
|
|
94
|
+
const serverProcess = spawn('node', ['build/index.js'], {
|
|
95
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
let response = '';
|
|
99
|
+
let error = '';
|
|
100
|
+
|
|
101
|
+
serverProcess.stdout.on('data', (data) => {
|
|
102
|
+
response += data.toString();
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
serverProcess.stderr.on('data', (data) => {
|
|
106
|
+
error += data.toString();
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
serverProcess.on('close', (code) => {
|
|
110
|
+
if (code !== 0) {
|
|
111
|
+
reject(new Error(`Server exited with code ${code}: ${error}`));
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
try {
|
|
116
|
+
// Parse JSON-RPC response
|
|
117
|
+
const lines = response.trim().split('\n');
|
|
118
|
+
let jsonResponse = null;
|
|
119
|
+
|
|
120
|
+
for (const line of lines) {
|
|
121
|
+
if (line.trim()) {
|
|
122
|
+
try {
|
|
123
|
+
const parsed = JSON.parse(line);
|
|
124
|
+
if (parsed.id === message.id) {
|
|
125
|
+
jsonResponse = parsed;
|
|
126
|
+
break;
|
|
127
|
+
}
|
|
128
|
+
} catch (e) {
|
|
129
|
+
// Skip non-JSON lines
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (jsonResponse) {
|
|
135
|
+
if (jsonResponse.error) {
|
|
136
|
+
reject(new Error(jsonResponse.error.message || 'Unknown error'));
|
|
137
|
+
} else {
|
|
138
|
+
resolve(jsonResponse.result);
|
|
139
|
+
}
|
|
140
|
+
} else {
|
|
141
|
+
reject(new Error(`No valid response found in: ${response}`));
|
|
142
|
+
}
|
|
143
|
+
} catch (e) {
|
|
144
|
+
reject(new Error(`Failed to parse response: ${e.message}\nResponse: ${response}`));
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
serverProcess.on('error', (err) => {
|
|
149
|
+
reject(new Error(`Failed to start server: ${err.message}`));
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
// Send the JSON-RPC message
|
|
153
|
+
serverProcess.stdin.write(JSON.stringify(message) + '\n');
|
|
154
|
+
serverProcess.stdin.end();
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Run resource tests
|
|
159
|
+
testResources().then(() => {
|
|
160
|
+
console.log('\nā
All resource tests completed!');
|
|
161
|
+
}).catch((error) => {
|
|
162
|
+
console.error('\nā Resource test suite failed:', error);
|
|
163
|
+
process.exit(1);
|
|
164
|
+
});
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { spawn } from 'child_process';
|
|
4
|
+
import { promises as fs } from 'fs';
|
|
5
|
+
|
|
6
|
+
// Simple MCP client test
|
|
7
|
+
async function testMCPServer() {
|
|
8
|
+
console.log('š§Ŗ Testing Incus MCP Server...\n');
|
|
9
|
+
|
|
10
|
+
// Test 1: List tools
|
|
11
|
+
console.log('1. Testing list tools...');
|
|
12
|
+
const listToolsMessage = {
|
|
13
|
+
jsonrpc: '2.0',
|
|
14
|
+
id: 1,
|
|
15
|
+
method: 'tools/list',
|
|
16
|
+
params: {}
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
const toolsResult = await sendMCPRequest(listToolsMessage);
|
|
21
|
+
console.log(` ā
Found ${toolsResult.tools.length} tools`);
|
|
22
|
+
toolsResult.tools.forEach(tool => {
|
|
23
|
+
console.log(` - ${tool.name}`);
|
|
24
|
+
});
|
|
25
|
+
} catch (error) {
|
|
26
|
+
console.log(` ā Error: ${error.message}`);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Test 2: List resources
|
|
30
|
+
console.log('\n2. Testing list resources...');
|
|
31
|
+
const listResourcesMessage = {
|
|
32
|
+
jsonrpc: '2.0',
|
|
33
|
+
id: 2,
|
|
34
|
+
method: 'resources/list',
|
|
35
|
+
params: {}
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
const resourcesResult = await sendMCPRequest(listResourcesMessage);
|
|
40
|
+
console.log(` ā
Found ${resourcesResult.resources.length} resources`);
|
|
41
|
+
resourcesResult.resources.forEach(resource => {
|
|
42
|
+
console.log(` - ${resource.uri}: ${resource.name}`);
|
|
43
|
+
});
|
|
44
|
+
} catch (error) {
|
|
45
|
+
console.log(` ā Error: ${error.message}`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Test 3: Test incus info tool
|
|
49
|
+
console.log('\n3. Testing incus_info tool...');
|
|
50
|
+
const infoMessage = {
|
|
51
|
+
jsonrpc: '2.0',
|
|
52
|
+
id: 3,
|
|
53
|
+
method: 'tools/call',
|
|
54
|
+
params: {
|
|
55
|
+
name: 'incus_info',
|
|
56
|
+
arguments: {}
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
const infoResult = await sendMCPRequest(infoMessage);
|
|
62
|
+
console.log(` ā
Got incus info`);
|
|
63
|
+
if (infoResult.content && infoResult.content[0]) {
|
|
64
|
+
console.log(` First few chars: ${infoResult.content[0].text.substring(0, 100)}...`);
|
|
65
|
+
}
|
|
66
|
+
} catch (error) {
|
|
67
|
+
console.log(` ā Error: ${error.message}`);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Test 4: Test list instances
|
|
71
|
+
console.log('\n4. Testing incus_list_instances tool...');
|
|
72
|
+
const listInstancesMessage = {
|
|
73
|
+
jsonrpc: '2.0',
|
|
74
|
+
id: 4,
|
|
75
|
+
method: 'tools/call',
|
|
76
|
+
params: {
|
|
77
|
+
name: 'incus_list_instances',
|
|
78
|
+
arguments: {}
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
const instancesResult = await sendMCPRequest(listInstancesMessage);
|
|
84
|
+
console.log(` ā
Got instances list`);
|
|
85
|
+
if (instancesResult.content && instancesResult.content[0]) {
|
|
86
|
+
console.log(` Response: ${instancesResult.content[0].text.substring(0, 200)}...`);
|
|
87
|
+
}
|
|
88
|
+
} catch (error) {
|
|
89
|
+
console.log(` ā Error: ${error.message}`);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Test 5: Test list remotes
|
|
93
|
+
console.log('\n5. Testing incus_list_remotes tool...');
|
|
94
|
+
const listRemotesMessage = {
|
|
95
|
+
jsonrpc: '2.0',
|
|
96
|
+
id: 5,
|
|
97
|
+
method: 'tools/call',
|
|
98
|
+
params: {
|
|
99
|
+
name: 'incus_list_remotes',
|
|
100
|
+
arguments: {}
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
try {
|
|
105
|
+
const remotesResult = await sendMCPRequest(listRemotesMessage);
|
|
106
|
+
console.log(` ā
Got remotes list`);
|
|
107
|
+
if (remotesResult.content && remotesResult.content[0]) {
|
|
108
|
+
console.log(` Response: ${remotesResult.content[0].text.substring(0, 200)}...`);
|
|
109
|
+
}
|
|
110
|
+
} catch (error) {
|
|
111
|
+
console.log(` ā Error: ${error.message}`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async function sendMCPRequest(message) {
|
|
116
|
+
return new Promise((resolve, reject) => {
|
|
117
|
+
const serverProcess = spawn('node', ['build/index.js'], {
|
|
118
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
let response = '';
|
|
122
|
+
let error = '';
|
|
123
|
+
|
|
124
|
+
serverProcess.stdout.on('data', (data) => {
|
|
125
|
+
response += data.toString();
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
serverProcess.stderr.on('data', (data) => {
|
|
129
|
+
error += data.toString();
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
serverProcess.on('close', (code) => {
|
|
133
|
+
if (code !== 0) {
|
|
134
|
+
reject(new Error(`Server exited with code ${code}: ${error}`));
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
try {
|
|
139
|
+
// Parse JSON-RPC response
|
|
140
|
+
const lines = response.trim().split('\n');
|
|
141
|
+
let jsonResponse = null;
|
|
142
|
+
|
|
143
|
+
for (const line of lines) {
|
|
144
|
+
if (line.trim()) {
|
|
145
|
+
try {
|
|
146
|
+
const parsed = JSON.parse(line);
|
|
147
|
+
if (parsed.id === message.id) {
|
|
148
|
+
jsonResponse = parsed;
|
|
149
|
+
break;
|
|
150
|
+
}
|
|
151
|
+
} catch (e) {
|
|
152
|
+
// Skip non-JSON lines
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (jsonResponse) {
|
|
158
|
+
if (jsonResponse.error) {
|
|
159
|
+
reject(new Error(jsonResponse.error.message || 'Unknown error'));
|
|
160
|
+
} else {
|
|
161
|
+
resolve(jsonResponse.result);
|
|
162
|
+
}
|
|
163
|
+
} else {
|
|
164
|
+
reject(new Error(`No valid response found in: ${response}`));
|
|
165
|
+
}
|
|
166
|
+
} catch (e) {
|
|
167
|
+
reject(new Error(`Failed to parse response: ${e.message}\nResponse: ${response}`));
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
serverProcess.on('error', (err) => {
|
|
172
|
+
reject(new Error(`Failed to start server: ${err.message}`));
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
// Send the JSON-RPC message
|
|
176
|
+
serverProcess.stdin.write(JSON.stringify(message) + '\n');
|
|
177
|
+
serverProcess.stdin.end();
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Run tests
|
|
182
|
+
testMCPServer().then(() => {
|
|
183
|
+
console.log('\nā
All tests completed!');
|
|
184
|
+
}).catch((error) => {
|
|
185
|
+
console.error('\nā Test suite failed:', error);
|
|
186
|
+
process.exit(1);
|
|
187
|
+
});
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "node",
|
|
6
|
+
"strict": true,
|
|
7
|
+
"esModuleInterop": true,
|
|
8
|
+
"skipLibCheck": true,
|
|
9
|
+
"forceConsistentCasingInFileNames": true,
|
|
10
|
+
"outDir": "./build",
|
|
11
|
+
"rootDir": "./src",
|
|
12
|
+
"declaration": true,
|
|
13
|
+
"declarationMap": true,
|
|
14
|
+
"sourceMap": true,
|
|
15
|
+
"resolveJsonModule": true,
|
|
16
|
+
"allowSyntheticDefaultImports": true
|
|
17
|
+
},
|
|
18
|
+
"include": ["src/**/*"],
|
|
19
|
+
"exclude": ["node_modules", "build", "**/*.test.ts"]
|
|
20
|
+
}
|