@abhishek8380/qconsul-mcp-server 1.0.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/.env.example +12 -0
- package/README.md +433 -0
- package/package.json +40 -0
- package/server.js +500 -0
package/.env.example
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# QConsul (Consul) Configuration
|
|
2
|
+
# Copy this file to .env and fill in your credentials
|
|
3
|
+
|
|
4
|
+
# Your Consul instance URL
|
|
5
|
+
CONSUL_URL=https://qconsul.p19.eng.sjc01.qualys.com
|
|
6
|
+
|
|
7
|
+
# Consul authentication credentials (HTTP Basic Auth)
|
|
8
|
+
CONSUL_USERNAME=admin
|
|
9
|
+
CONSUL_PASSWORD=admin
|
|
10
|
+
|
|
11
|
+
# Default datacenter (e.g., eng-sjc01-p19)
|
|
12
|
+
CONSUL_DATACENTER=eng-sjc01-p19
|
package/README.md
ADDED
|
@@ -0,0 +1,433 @@
|
|
|
1
|
+
# Qualys QConsul MCP Server
|
|
2
|
+
|
|
3
|
+
A Model Context Protocol (MCP) server for HashiCorp Consul KV store operations. This server enables AI assistants to interact with Consul's key-value store through a standardized interface, allowing you to read, write, update, and delete configuration values directly from your IDE.
|
|
4
|
+
|
|
5
|
+
**Built for Qualys infrastructure teams** to manage Consul KV configurations seamlessly.
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- 🔍 **Search and list keys** - Discover keys with prefix matching and search
|
|
10
|
+
- 📖 **Read key values** - Get individual or recursive key-value pairs
|
|
11
|
+
- ✏️ **Create/Update keys** - Set or modify key-value pairs
|
|
12
|
+
- 🗑️ **Delete keys** - Remove single keys or entire prefixes recursively
|
|
13
|
+
- 🔐 **HTTP Basic Authentication** - Secure access with username/password
|
|
14
|
+
- 🧠 **Dual naming** - Every tool available under both `*` and `qconsul_*` aliases
|
|
15
|
+
- 🚀 **Zero installation** - Use with npx in your MCP client configuration
|
|
16
|
+
|
|
17
|
+
## Use Case
|
|
18
|
+
|
|
19
|
+
This MCP server is designed for teams using Consul KV store for configuration management. It allows you to:
|
|
20
|
+
|
|
21
|
+
- **Update configuration files** directly from your IDE without switching to the Consul UI
|
|
22
|
+
- **Search and discover** configuration keys across your infrastructure
|
|
23
|
+
- **Bulk operations** on configuration hierarchies
|
|
24
|
+
- **Version control integration** - Make config changes alongside code changes
|
|
25
|
+
|
|
26
|
+
Perfect for DevOps teams managing microservices configuration in Consul.
|
|
27
|
+
|
|
28
|
+
## Installation
|
|
29
|
+
|
|
30
|
+
No installation required! Use with npx in your MCP client configuration.
|
|
31
|
+
|
|
32
|
+
## Configuration for Windsurf
|
|
33
|
+
|
|
34
|
+
Add this to your Windsurf MCP configuration file (`~/.codeium/windsurf/mcp_config.json`):
|
|
35
|
+
|
|
36
|
+
```json
|
|
37
|
+
{
|
|
38
|
+
"mcpServers": {
|
|
39
|
+
"qconsul": {
|
|
40
|
+
"command": "npx",
|
|
41
|
+
"args": ["-y", "@abhishek8380/qconsul-mcp-server"],
|
|
42
|
+
"env": {
|
|
43
|
+
"CONSUL_URL": "https://qconsul.p19.eng.sjc01.qualys.com",
|
|
44
|
+
"CONSUL_USERNAME": "your-username",
|
|
45
|
+
"CONSUL_PASSWORD": "your-password",
|
|
46
|
+
"CONSUL_DATACENTER": "eng-sjc01-p19"
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### Environment Variables
|
|
54
|
+
|
|
55
|
+
- `CONSUL_URL` - Your Consul instance URL (e.g., `https://qconsul.p19.eng.sjc01.qualys.com`)
|
|
56
|
+
- `CONSUL_USERNAME` - HTTP Basic Auth username (required)
|
|
57
|
+
- `CONSUL_PASSWORD` - HTTP Basic Auth password (required)
|
|
58
|
+
- `CONSUL_DATACENTER` - Default datacenter to use (e.g., `eng-sjc01-p19`)
|
|
59
|
+
|
|
60
|
+
## Configuration for Claude Desktop
|
|
61
|
+
|
|
62
|
+
Add to `~/Library/Application Support/Claude/claude_desktop_config.json`:
|
|
63
|
+
|
|
64
|
+
```json
|
|
65
|
+
{
|
|
66
|
+
"mcpServers": {
|
|
67
|
+
"qconsul": {
|
|
68
|
+
"command": "npx",
|
|
69
|
+
"args": ["-y", "@abhishek8380/qconsul-mcp-server"],
|
|
70
|
+
"env": {
|
|
71
|
+
"CONSUL_URL": "https://qconsul.p19.eng.sjc01.qualys.com",
|
|
72
|
+
"CONSUL_USERNAME": "your-username",
|
|
73
|
+
"CONSUL_PASSWORD": "your-password",
|
|
74
|
+
"CONSUL_DATACENTER": "eng-sjc01-p19"
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Available Tools
|
|
82
|
+
|
|
83
|
+
Every tool below can be called with either its standard name or the `qconsul_*` alias shown in parentheses.
|
|
84
|
+
|
|
85
|
+
### Read Operations
|
|
86
|
+
|
|
87
|
+
#### **get_key** (`qconsul_get_key`)
|
|
88
|
+
Retrieve a value from Consul KV store by key path.
|
|
89
|
+
|
|
90
|
+
**Parameters:**
|
|
91
|
+
- `key` (string, required) - The key path to retrieve (e.g., `cloudview/config/setting`)
|
|
92
|
+
- `raw` (boolean, optional) - Return raw value without metadata (default: `false`)
|
|
93
|
+
- `datacenter` (string, optional) - Datacenter to query
|
|
94
|
+
|
|
95
|
+
**Example:**
|
|
96
|
+
```javascript
|
|
97
|
+
{
|
|
98
|
+
"key": "cloudview/config/database",
|
|
99
|
+
"raw": false
|
|
100
|
+
}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
#### **list_keys** (`qconsul_list_keys`)
|
|
104
|
+
List all keys under a prefix in Consul KV store.
|
|
105
|
+
|
|
106
|
+
**Parameters:**
|
|
107
|
+
- `prefix` (string, optional) - Key prefix to list (empty for all keys)
|
|
108
|
+
- `separator` (string, optional) - Separator for grouping keys (e.g., `/`)
|
|
109
|
+
- `datacenter` (string, optional) - Datacenter to query
|
|
110
|
+
|
|
111
|
+
**Example:**
|
|
112
|
+
```javascript
|
|
113
|
+
{
|
|
114
|
+
"prefix": "cloudview/",
|
|
115
|
+
"separator": "/"
|
|
116
|
+
}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
#### **get_keys_recursive** (`qconsul_get_keys_recursive`)
|
|
120
|
+
Retrieve all key-value pairs under a prefix recursively.
|
|
121
|
+
|
|
122
|
+
**Parameters:**
|
|
123
|
+
- `prefix` (string, optional) - Key prefix to retrieve (empty for all keys)
|
|
124
|
+
- `datacenter` (string, optional) - Datacenter to query
|
|
125
|
+
|
|
126
|
+
**Example:**
|
|
127
|
+
```javascript
|
|
128
|
+
{
|
|
129
|
+
"prefix": "cloudview/config/"
|
|
130
|
+
}
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
#### **search_keys** (`qconsul_search_keys`)
|
|
134
|
+
Search for keys matching a pattern in Consul KV store.
|
|
135
|
+
|
|
136
|
+
**Parameters:**
|
|
137
|
+
- `searchTerm` (string, required) - Search term to find in key names
|
|
138
|
+
- `prefix` (string, optional) - Optional prefix to narrow search scope
|
|
139
|
+
- `datacenter` (string, optional) - Datacenter to query
|
|
140
|
+
|
|
141
|
+
**Example:**
|
|
142
|
+
```javascript
|
|
143
|
+
{
|
|
144
|
+
"searchTerm": "database",
|
|
145
|
+
"prefix": "cloudview/"
|
|
146
|
+
}
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
### Write Operations
|
|
150
|
+
|
|
151
|
+
#### **put_key** (`qconsul_put_key`)
|
|
152
|
+
Create or update a key-value pair in Consul KV store.
|
|
153
|
+
|
|
154
|
+
**Parameters:**
|
|
155
|
+
- `key` (string, required) - The key path to create/update (e.g., `cloudview/config/setting`)
|
|
156
|
+
- `value` (string, required) - The value to store
|
|
157
|
+
- `datacenter` (string, optional) - Datacenter to use
|
|
158
|
+
- `flags` (number, optional) - Optional flags for the key (default: `0`)
|
|
159
|
+
|
|
160
|
+
**Example:**
|
|
161
|
+
```javascript
|
|
162
|
+
{
|
|
163
|
+
"key": "cloudview/config/timeout",
|
|
164
|
+
"value": "30"
|
|
165
|
+
}
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
### Delete Operations
|
|
169
|
+
|
|
170
|
+
#### **delete_key** (`qconsul_delete_key`)
|
|
171
|
+
Delete a key or keys from Consul KV store.
|
|
172
|
+
|
|
173
|
+
**Parameters:**
|
|
174
|
+
- `key` (string, required) - The key path to delete
|
|
175
|
+
- `recurse` (boolean, optional) - Delete all keys with this prefix (default: `false`)
|
|
176
|
+
- `datacenter` (string, optional) - Datacenter to use
|
|
177
|
+
|
|
178
|
+
**Example:**
|
|
179
|
+
```javascript
|
|
180
|
+
{
|
|
181
|
+
"key": "cloudview/config/old-setting",
|
|
182
|
+
"recurse": false
|
|
183
|
+
}
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
## How to Use - CRUD Operations Guide
|
|
187
|
+
|
|
188
|
+
### 1. **CREATE** - Add New Configuration
|
|
189
|
+
|
|
190
|
+
**Create a single key:**
|
|
191
|
+
```
|
|
192
|
+
Ask: "Create a new key cloudview/config/new-feature with value 'enabled'"
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
**Create multiple keys (bulk upload):**
|
|
196
|
+
```
|
|
197
|
+
Ask: "Create these keys under cloudview/myapp/:
|
|
198
|
+
- config.yml with database settings
|
|
199
|
+
- kafka-client.yml with broker configs
|
|
200
|
+
- application.properties with app settings"
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
**What happens:** The MCP server uses `put_key` to create new entries in Consul KV store.
|
|
204
|
+
|
|
205
|
+
---
|
|
206
|
+
|
|
207
|
+
### 2. **READ** - Retrieve Configuration
|
|
208
|
+
|
|
209
|
+
**Read a single key:**
|
|
210
|
+
```
|
|
211
|
+
Ask: "Get the value of cloudview/config/database"
|
|
212
|
+
Ask: "Show me cloudview/gcp-service/application.yml"
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
**List all keys under a prefix:**
|
|
216
|
+
```
|
|
217
|
+
Ask: "List all keys under cloudview/"
|
|
218
|
+
Ask: "Show me all configuration files in cloudview/myapp/"
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
**Search for specific keys:**
|
|
222
|
+
```
|
|
223
|
+
Ask: "Search for keys containing 'database' in cloudview"
|
|
224
|
+
Ask: "Find all kafka configuration keys"
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
**Get all keys recursively with values:**
|
|
228
|
+
```
|
|
229
|
+
Ask: "Get all key-value pairs under cloudview/config/"
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
**What happens:** The MCP server uses `get_key`, `list_keys`, `search_keys`, or `get_keys_recursive` to fetch data.
|
|
233
|
+
|
|
234
|
+
---
|
|
235
|
+
|
|
236
|
+
### 3. **UPDATE** - Modify Existing Configuration
|
|
237
|
+
|
|
238
|
+
**Update a single value:**
|
|
239
|
+
```
|
|
240
|
+
Ask: "Update cloudview/config/timeout to 60"
|
|
241
|
+
Ask: "Change the version in cloudview/gcp-service/application.yml to 2.8.0"
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
**Update entire file:**
|
|
245
|
+
```
|
|
246
|
+
Ask: "Update cloudview/config/database.yml with this content:
|
|
247
|
+
host: db.example.com
|
|
248
|
+
port: 5432
|
|
249
|
+
username: dbuser"
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
**What happens:** The MCP server:
|
|
253
|
+
1. Reads the current value (if needed)
|
|
254
|
+
2. Modifies the specific field or replaces entire content
|
|
255
|
+
3. Uses `put_key` to update the value in Consul
|
|
256
|
+
|
|
257
|
+
---
|
|
258
|
+
|
|
259
|
+
### 4. **DELETE** - Remove Configuration
|
|
260
|
+
|
|
261
|
+
**Delete a single key:**
|
|
262
|
+
```
|
|
263
|
+
Ask: "Delete the key cloudview/config/deprecated-setting"
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
**Delete all keys under a prefix (recursive):**
|
|
267
|
+
```
|
|
268
|
+
Ask: "Delete all keys under cloudview/old-service/ recursively"
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
**What happens:** The MCP server uses `delete_key` with optional `recurse` parameter.
|
|
272
|
+
|
|
273
|
+
---
|
|
274
|
+
|
|
275
|
+
## Complete Example Workflow
|
|
276
|
+
|
|
277
|
+
### Scenario: Managing a new microservice configuration
|
|
278
|
+
|
|
279
|
+
**Step 1: Discover existing structure**
|
|
280
|
+
```
|
|
281
|
+
Ask: "List all keys under cloudview/"
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
**Step 2: Create new service configuration**
|
|
285
|
+
```
|
|
286
|
+
Ask: "Create cloudview/my-service/application.yml with:
|
|
287
|
+
server:
|
|
288
|
+
port: 8080
|
|
289
|
+
database:
|
|
290
|
+
url: jdbc:postgresql://localhost:5432/mydb"
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
**Step 3: Verify creation**
|
|
294
|
+
```
|
|
295
|
+
Ask: "Get the value of cloudview/my-service/application.yml"
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
**Step 4: Update a specific value**
|
|
299
|
+
```
|
|
300
|
+
Ask: "Update the port in cloudview/my-service/application.yml to 9090"
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
**Step 5: Add more configuration files**
|
|
304
|
+
```
|
|
305
|
+
Ask: "Create cloudview/my-service/kafka-client.yml with broker configs"
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
**Step 6: List all service configs**
|
|
309
|
+
```
|
|
310
|
+
Ask: "List all keys under cloudview/my-service/"
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
**Step 7: Clean up (if needed)**
|
|
314
|
+
```
|
|
315
|
+
Ask: "Delete cloudview/my-service/old-config.yml"
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
## Authentication
|
|
319
|
+
|
|
320
|
+
This server uses **HTTP Basic Authentication** to connect to your Consul instance. The credentials are passed via environment variables:
|
|
321
|
+
|
|
322
|
+
- Username and password are base64-encoded and sent in the `Authorization` header
|
|
323
|
+
- Ensure your Consul instance is configured to accept Basic Auth
|
|
324
|
+
- For production use, consider using ACL tokens instead of basic auth
|
|
325
|
+
|
|
326
|
+
## Consul KV Hierarchy
|
|
327
|
+
|
|
328
|
+
Consul KV uses a hierarchical key structure with `/` as the separator:
|
|
329
|
+
|
|
330
|
+
```
|
|
331
|
+
cloudview/
|
|
332
|
+
├── config/
|
|
333
|
+
│ ├── database
|
|
334
|
+
│ ├── timeout
|
|
335
|
+
│ └── api-key
|
|
336
|
+
├── features/
|
|
337
|
+
│ ├── feature-a
|
|
338
|
+
│ └── feature-b
|
|
339
|
+
└── metadata/
|
|
340
|
+
└── version
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
Keys are case-sensitive and can contain any UTF-8 characters.
|
|
344
|
+
|
|
345
|
+
## Local Development
|
|
346
|
+
|
|
347
|
+
1. Clone the repository
|
|
348
|
+
2. Install dependencies:
|
|
349
|
+
```bash
|
|
350
|
+
npm install
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
3. Create `.env` file:
|
|
354
|
+
```bash
|
|
355
|
+
cp .env.example .env
|
|
356
|
+
# Edit .env with your credentials
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
4. Run the server:
|
|
360
|
+
```bash
|
|
361
|
+
npm start
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
## Testing the Server
|
|
365
|
+
|
|
366
|
+
You can test the server locally using the MCP Inspector or by configuring it in Windsurf/Claude Desktop.
|
|
367
|
+
|
|
368
|
+
### Example Commands
|
|
369
|
+
|
|
370
|
+
```bash
|
|
371
|
+
# List all keys
|
|
372
|
+
curl -u username:password https://qconsul.p19.eng.sjc01.qualys.com/v1/kv/?keys=true
|
|
373
|
+
|
|
374
|
+
# Get a specific key
|
|
375
|
+
curl -u username:password https://qconsul.p19.eng.sjc01.qualys.com/v1/kv/cloudview/config/setting
|
|
376
|
+
|
|
377
|
+
# Put a key
|
|
378
|
+
curl -u username:password -X PUT -d "value" https://qconsul.p19.eng.sjc01.qualys.com/v1/kv/cloudview/config/setting
|
|
379
|
+
|
|
380
|
+
# Delete a key
|
|
381
|
+
curl -u username:password -X DELETE https://qconsul.p19.eng.sjc01.qualys.com/v1/kv/cloudview/config/setting
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
## Security Considerations
|
|
385
|
+
|
|
386
|
+
- **Never commit credentials** - Use environment variables or secure secret management
|
|
387
|
+
- **Use HTTPS** - Always connect to Consul over HTTPS in production
|
|
388
|
+
- **ACL Tokens** - Consider using Consul ACL tokens instead of basic auth for production
|
|
389
|
+
- **Least Privilege** - Grant only necessary permissions to the MCP server
|
|
390
|
+
|
|
391
|
+
## Troubleshooting
|
|
392
|
+
|
|
393
|
+
### Connection Issues
|
|
394
|
+
- Verify `CONSUL_URL` is correct and accessible
|
|
395
|
+
- Check that your credentials are valid
|
|
396
|
+
- Ensure your network allows access to the Consul instance
|
|
397
|
+
|
|
398
|
+
### Authentication Errors
|
|
399
|
+
- Confirm username and password are correct
|
|
400
|
+
- Check if Consul requires ACL tokens instead of basic auth
|
|
401
|
+
|
|
402
|
+
### Key Not Found
|
|
403
|
+
- Verify the key path is correct (case-sensitive)
|
|
404
|
+
- Use `list_keys` to discover available keys
|
|
405
|
+
- Check the datacenter parameter if using multiple datacenters
|
|
406
|
+
|
|
407
|
+
## API Reference
|
|
408
|
+
|
|
409
|
+
This server implements the Consul KV HTTP API:
|
|
410
|
+
- [Consul KV Store API Documentation](https://developer.hashicorp.com/consul/api-docs/kv)
|
|
411
|
+
- [Consul KV CLI Reference](https://developer.hashicorp.com/consul/commands/kv)
|
|
412
|
+
|
|
413
|
+
## License
|
|
414
|
+
|
|
415
|
+
MIT
|
|
416
|
+
|
|
417
|
+
## Author
|
|
418
|
+
|
|
419
|
+
Abhishek Bhadane
|
|
420
|
+
|
|
421
|
+
## Package
|
|
422
|
+
|
|
423
|
+
https://www.npmjs.com/package/@abhishek8380/qconsul-mcp-server
|
|
424
|
+
|
|
425
|
+
## Contributing
|
|
426
|
+
|
|
427
|
+
Contributions are welcome! Please feel free to submit a Pull Request.
|
|
428
|
+
|
|
429
|
+
## Support
|
|
430
|
+
|
|
431
|
+
For issues and questions:
|
|
432
|
+
- GitHub Issues: [Create an issue](https://github.com/yourusername/qconsul-mcp-server/issues)
|
|
433
|
+
- Documentation: [Consul KV Store](https://developer.hashicorp.com/consul/docs/automate/kv)
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@abhishek8380/qconsul-mcp-server",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "MCP server for Consul KV store operations (get, put, delete, list keys)",
|
|
6
|
+
"main": "server.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"qconsul-mcp": "./server.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"start": "node server.js"
|
|
12
|
+
},
|
|
13
|
+
"keywords": [
|
|
14
|
+
"mcp",
|
|
15
|
+
"consul",
|
|
16
|
+
"qconsul",
|
|
17
|
+
"kv",
|
|
18
|
+
"key-value",
|
|
19
|
+
"stdio",
|
|
20
|
+
"model-context-protocol",
|
|
21
|
+
"windsurf",
|
|
22
|
+
"hashicorp"
|
|
23
|
+
],
|
|
24
|
+
"author": "Abhishek Bhadane",
|
|
25
|
+
"license": "MIT",
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
28
|
+
"node-fetch": "^3.3.2",
|
|
29
|
+
"dotenv": "^16.3.1",
|
|
30
|
+
"zod": "^3.22.4"
|
|
31
|
+
},
|
|
32
|
+
"engines": {
|
|
33
|
+
"node": ">=18.0.0"
|
|
34
|
+
},
|
|
35
|
+
"files": [
|
|
36
|
+
"server.js",
|
|
37
|
+
"README.md",
|
|
38
|
+
".env.example"
|
|
39
|
+
]
|
|
40
|
+
}
|
package/server.js
ADDED
|
@@ -0,0 +1,500 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
4
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
5
|
+
import fetch from "node-fetch";
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
import dotenv from "dotenv";
|
|
8
|
+
import https from "https";
|
|
9
|
+
|
|
10
|
+
dotenv.config();
|
|
11
|
+
|
|
12
|
+
const httpsAgent = new https.Agent({
|
|
13
|
+
rejectUnauthorized: false
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
const CONSUL_URL = process.env.CONSUL_URL
|
|
17
|
+
const CONSUL_USERNAME = process.env.CONSUL_USERNAME
|
|
18
|
+
const CONSUL_PASSWORD = process.env.CONSUL_PASSWORD
|
|
19
|
+
const CONSUL_DATACENTER = process.env.CONSUL_DATACENTER;
|
|
20
|
+
|
|
21
|
+
console.error(`🔐 Consul URL: ${CONSUL_URL}`);
|
|
22
|
+
console.error(`🔐 Datacenter: ${CONSUL_DATACENTER}`);
|
|
23
|
+
console.error(`🔐 Username: ${CONSUL_USERNAME}`);
|
|
24
|
+
|
|
25
|
+
async function makeAuthenticatedRequest(url, options = {}) {
|
|
26
|
+
const authString = Buffer.from(`${CONSUL_USERNAME}:${CONSUL_PASSWORD}`).toString('base64');
|
|
27
|
+
|
|
28
|
+
const defaultHeaders = {
|
|
29
|
+
"Accept": "application/json",
|
|
30
|
+
"Content-Type": "application/json",
|
|
31
|
+
"Authorization": `Basic ${authString}`,
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const requestOptions = {
|
|
35
|
+
...options,
|
|
36
|
+
agent: httpsAgent,
|
|
37
|
+
headers: {
|
|
38
|
+
...defaultHeaders,
|
|
39
|
+
...options.headers
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const response = await fetch(url, requestOptions);
|
|
44
|
+
return response;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const server = new McpServer({
|
|
48
|
+
name: "qconsul-mcp-server",
|
|
49
|
+
version: "1.0.0",
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
const onboardingTip = [
|
|
53
|
+
"💡 QConsul (Consul KV) tip:",
|
|
54
|
+
"Use list_keys to discover available keys before reading or updating.",
|
|
55
|
+
"Keys are hierarchical (e.g., cloudview/config/setting)."
|
|
56
|
+
].join("\n");
|
|
57
|
+
|
|
58
|
+
console.error(onboardingTip);
|
|
59
|
+
|
|
60
|
+
function registerQConsulTool(toolNames, config, handler) {
|
|
61
|
+
const names = Array.isArray(toolNames) ? toolNames : [toolNames];
|
|
62
|
+
names.forEach((toolName) => {
|
|
63
|
+
server.registerTool(toolName, { ...config }, handler);
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
registerQConsulTool([
|
|
68
|
+
"get_key",
|
|
69
|
+
"qconsul_get_key"
|
|
70
|
+
],
|
|
71
|
+
{
|
|
72
|
+
title: "Get Consul KV Key",
|
|
73
|
+
description: "Retrieve a value from Consul KV store by key path",
|
|
74
|
+
inputSchema: z.object({
|
|
75
|
+
key: z.string().describe("The key path to retrieve (e.g., cloudview/config/setting)"),
|
|
76
|
+
raw: z.boolean().optional().default(false).describe("Return raw value without metadata (default: false)"),
|
|
77
|
+
datacenter: z.string().optional().describe("Datacenter to query (optional)")
|
|
78
|
+
}),
|
|
79
|
+
},
|
|
80
|
+
async ({ key, raw, datacenter }) => {
|
|
81
|
+
console.error(`📖 Getting key: ${key}`);
|
|
82
|
+
|
|
83
|
+
try {
|
|
84
|
+
const dc = datacenter || CONSUL_DATACENTER;
|
|
85
|
+
const rawParam = raw ? "&raw=true" : "";
|
|
86
|
+
const url = `${CONSUL_URL}/v1/kv/${key}?dc=${dc}${rawParam}`;
|
|
87
|
+
|
|
88
|
+
const response = await makeAuthenticatedRequest(url);
|
|
89
|
+
|
|
90
|
+
if (response.status === 404) {
|
|
91
|
+
return {
|
|
92
|
+
content: [
|
|
93
|
+
{
|
|
94
|
+
type: "text",
|
|
95
|
+
text: `❌ Key not found: ${key}`,
|
|
96
|
+
},
|
|
97
|
+
],
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (!response.ok) {
|
|
102
|
+
throw new Error(`HTTP error! status: ${response.status}`);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (raw) {
|
|
106
|
+
const rawValue = await response.text();
|
|
107
|
+
return {
|
|
108
|
+
content: [
|
|
109
|
+
{
|
|
110
|
+
type: "text",
|
|
111
|
+
text: rawValue,
|
|
112
|
+
},
|
|
113
|
+
],
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const data = await response.json();
|
|
118
|
+
const kvData = data[0];
|
|
119
|
+
|
|
120
|
+
const decodedValue = kvData.Value ? Buffer.from(kvData.Value, 'base64').toString('utf-8') : null;
|
|
121
|
+
|
|
122
|
+
const keyInfo = {
|
|
123
|
+
key: kvData.Key,
|
|
124
|
+
value: decodedValue,
|
|
125
|
+
createIndex: kvData.CreateIndex,
|
|
126
|
+
modifyIndex: kvData.ModifyIndex,
|
|
127
|
+
lockIndex: kvData.LockIndex,
|
|
128
|
+
flags: kvData.Flags
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
return {
|
|
132
|
+
content: [
|
|
133
|
+
{
|
|
134
|
+
type: "text",
|
|
135
|
+
text: JSON.stringify(keyInfo, null, 2),
|
|
136
|
+
},
|
|
137
|
+
],
|
|
138
|
+
};
|
|
139
|
+
} catch (error) {
|
|
140
|
+
console.error("❌ Error getting key:", error);
|
|
141
|
+
return {
|
|
142
|
+
content: [
|
|
143
|
+
{
|
|
144
|
+
type: "text",
|
|
145
|
+
text: `Error getting key: ${error.message}`,
|
|
146
|
+
},
|
|
147
|
+
],
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
registerQConsulTool([
|
|
154
|
+
"put_key",
|
|
155
|
+
"qconsul_put_key"
|
|
156
|
+
],
|
|
157
|
+
{
|
|
158
|
+
title: "Put/Update Consul KV Key",
|
|
159
|
+
description: "Create or update a key-value pair in Consul KV store",
|
|
160
|
+
inputSchema: z.object({
|
|
161
|
+
key: z.string().describe("The key path to create/update (e.g., cloudview/config/setting)"),
|
|
162
|
+
value: z.string().describe("The value to store"),
|
|
163
|
+
datacenter: z.string().optional().describe("Datacenter to use (optional)"),
|
|
164
|
+
flags: z.number().optional().describe("Optional flags for the key (default: 0)")
|
|
165
|
+
}),
|
|
166
|
+
},
|
|
167
|
+
async ({ key, value, datacenter, flags }) => {
|
|
168
|
+
console.error(`✏️ Putting key: ${key}`);
|
|
169
|
+
|
|
170
|
+
try {
|
|
171
|
+
const dc = datacenter || CONSUL_DATACENTER;
|
|
172
|
+
const flagsParam = flags !== undefined ? `&flags=${flags}` : "";
|
|
173
|
+
const url = `${CONSUL_URL}/v1/kv/${key}?dc=${dc}${flagsParam}`;
|
|
174
|
+
|
|
175
|
+
const response = await makeAuthenticatedRequest(url, {
|
|
176
|
+
method: "PUT",
|
|
177
|
+
body: value,
|
|
178
|
+
headers: {
|
|
179
|
+
"Content-Type": "text/plain"
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
if (!response.ok) {
|
|
184
|
+
const errorText = await response.text();
|
|
185
|
+
throw new Error(`HTTP error! status: ${response.status}, details: ${errorText}`);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const success = await response.json();
|
|
189
|
+
|
|
190
|
+
if (success) {
|
|
191
|
+
return {
|
|
192
|
+
content: [
|
|
193
|
+
{
|
|
194
|
+
type: "text",
|
|
195
|
+
text: `✅ Key updated successfully!\n\nKey: ${key}\nValue: ${value}\nDatacenter: ${dc}`,
|
|
196
|
+
},
|
|
197
|
+
],
|
|
198
|
+
};
|
|
199
|
+
} else {
|
|
200
|
+
return {
|
|
201
|
+
content: [
|
|
202
|
+
{
|
|
203
|
+
type: "text",
|
|
204
|
+
text: `⚠️ Key update returned false. This may indicate a CAS failure or other issue.`,
|
|
205
|
+
},
|
|
206
|
+
],
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
} catch (error) {
|
|
210
|
+
console.error("❌ Error putting key:", error);
|
|
211
|
+
return {
|
|
212
|
+
content: [
|
|
213
|
+
{
|
|
214
|
+
type: "text",
|
|
215
|
+
text: `Error putting key: ${error.message}`,
|
|
216
|
+
},
|
|
217
|
+
],
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
);
|
|
222
|
+
|
|
223
|
+
registerQConsulTool([
|
|
224
|
+
"delete_key",
|
|
225
|
+
"qconsul_delete_key"
|
|
226
|
+
],
|
|
227
|
+
{
|
|
228
|
+
title: "Delete Consul KV Key",
|
|
229
|
+
description: "Delete a key or keys from Consul KV store",
|
|
230
|
+
inputSchema: z.object({
|
|
231
|
+
key: z.string().describe("The key path to delete"),
|
|
232
|
+
recurse: z.boolean().optional().default(false).describe("Delete all keys with this prefix (default: false)"),
|
|
233
|
+
datacenter: z.string().optional().describe("Datacenter to use (optional)")
|
|
234
|
+
}),
|
|
235
|
+
},
|
|
236
|
+
async ({ key, recurse, datacenter }) => {
|
|
237
|
+
console.error(`🗑️ Deleting key: ${key}${recurse ? ' (recursive)' : ''}`);
|
|
238
|
+
|
|
239
|
+
try {
|
|
240
|
+
const dc = datacenter || CONSUL_DATACENTER;
|
|
241
|
+
const recurseParam = recurse ? "&recurse=true" : "";
|
|
242
|
+
const url = `${CONSUL_URL}/v1/kv/${key}?dc=${dc}${recurseParam}`;
|
|
243
|
+
|
|
244
|
+
const response = await makeAuthenticatedRequest(url, {
|
|
245
|
+
method: "DELETE"
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
if (!response.ok) {
|
|
249
|
+
throw new Error(`HTTP error! status: ${response.status}`);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const success = await response.json();
|
|
253
|
+
|
|
254
|
+
if (success) {
|
|
255
|
+
return {
|
|
256
|
+
content: [
|
|
257
|
+
{
|
|
258
|
+
type: "text",
|
|
259
|
+
text: `✅ Key deleted successfully!\n\nKey: ${key}${recurse ? '\nMode: Recursive (all keys with this prefix)' : ''}`,
|
|
260
|
+
},
|
|
261
|
+
],
|
|
262
|
+
};
|
|
263
|
+
} else {
|
|
264
|
+
return {
|
|
265
|
+
content: [
|
|
266
|
+
{
|
|
267
|
+
type: "text",
|
|
268
|
+
text: `⚠️ Key deletion returned false. Key may not exist.`,
|
|
269
|
+
},
|
|
270
|
+
],
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
} catch (error) {
|
|
274
|
+
console.error("❌ Error deleting key:", error);
|
|
275
|
+
return {
|
|
276
|
+
content: [
|
|
277
|
+
{
|
|
278
|
+
type: "text",
|
|
279
|
+
text: `Error deleting key: ${error.message}`,
|
|
280
|
+
},
|
|
281
|
+
],
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
);
|
|
286
|
+
|
|
287
|
+
registerQConsulTool([
|
|
288
|
+
"list_keys",
|
|
289
|
+
"qconsul_list_keys"
|
|
290
|
+
],
|
|
291
|
+
{
|
|
292
|
+
title: "List Consul KV Keys",
|
|
293
|
+
description: "List all keys under a prefix in Consul KV store",
|
|
294
|
+
inputSchema: z.object({
|
|
295
|
+
prefix: z.string().optional().default("").describe("Key prefix to list (empty for all keys)"),
|
|
296
|
+
separator: z.string().optional().describe("Separator for grouping keys (e.g., '/')"),
|
|
297
|
+
datacenter: z.string().optional().describe("Datacenter to query (optional)")
|
|
298
|
+
}),
|
|
299
|
+
},
|
|
300
|
+
async ({ prefix, separator, datacenter }) => {
|
|
301
|
+
console.error(`📋 Listing keys with prefix: ${prefix || '(all)'}`);
|
|
302
|
+
|
|
303
|
+
try {
|
|
304
|
+
const dc = datacenter || CONSUL_DATACENTER;
|
|
305
|
+
const separatorParam = separator ? `&separator=${encodeURIComponent(separator)}` : "";
|
|
306
|
+
const url = `${CONSUL_URL}/v1/kv/${prefix}?dc=${dc}&keys=true${separatorParam}`;
|
|
307
|
+
|
|
308
|
+
const response = await makeAuthenticatedRequest(url);
|
|
309
|
+
|
|
310
|
+
if (response.status === 404) {
|
|
311
|
+
return {
|
|
312
|
+
content: [
|
|
313
|
+
{
|
|
314
|
+
type: "text",
|
|
315
|
+
text: `No keys found with prefix: ${prefix || '(root)'}`,
|
|
316
|
+
},
|
|
317
|
+
],
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
if (!response.ok) {
|
|
322
|
+
throw new Error(`HTTP error! status: ${response.status}`);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const keys = await response.json();
|
|
326
|
+
|
|
327
|
+
return {
|
|
328
|
+
content: [
|
|
329
|
+
{
|
|
330
|
+
type: "text",
|
|
331
|
+
text: `Found ${keys.length} key(s)${prefix ? ` with prefix "${prefix}"` : ''}:\n\n${JSON.stringify(keys, null, 2)}`,
|
|
332
|
+
},
|
|
333
|
+
],
|
|
334
|
+
};
|
|
335
|
+
} catch (error) {
|
|
336
|
+
console.error("❌ Error listing keys:", error);
|
|
337
|
+
return {
|
|
338
|
+
content: [
|
|
339
|
+
{
|
|
340
|
+
type: "text",
|
|
341
|
+
text: `Error listing keys: ${error.message}`,
|
|
342
|
+
},
|
|
343
|
+
],
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
);
|
|
348
|
+
|
|
349
|
+
registerQConsulTool([
|
|
350
|
+
"get_keys_recursive",
|
|
351
|
+
"qconsul_get_keys_recursive"
|
|
352
|
+
],
|
|
353
|
+
{
|
|
354
|
+
title: "Get All Keys and Values Recursively",
|
|
355
|
+
description: "Retrieve all key-value pairs under a prefix (recursive)",
|
|
356
|
+
inputSchema: z.object({
|
|
357
|
+
prefix: z.string().optional().default("").describe("Key prefix to retrieve (empty for all keys)"),
|
|
358
|
+
datacenter: z.string().optional().describe("Datacenter to query (optional)")
|
|
359
|
+
}),
|
|
360
|
+
},
|
|
361
|
+
async ({ prefix, datacenter }) => {
|
|
362
|
+
console.error(`📦 Getting all keys recursively with prefix: ${prefix || '(all)'}`);
|
|
363
|
+
|
|
364
|
+
try {
|
|
365
|
+
const dc = datacenter || CONSUL_DATACENTER;
|
|
366
|
+
const url = `${CONSUL_URL}/v1/kv/${prefix}?dc=${dc}&recurse=true`;
|
|
367
|
+
|
|
368
|
+
const response = await makeAuthenticatedRequest(url);
|
|
369
|
+
|
|
370
|
+
if (response.status === 404) {
|
|
371
|
+
return {
|
|
372
|
+
content: [
|
|
373
|
+
{
|
|
374
|
+
type: "text",
|
|
375
|
+
text: `No keys found with prefix: ${prefix || '(root)'}`,
|
|
376
|
+
},
|
|
377
|
+
],
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
if (!response.ok) {
|
|
382
|
+
throw new Error(`HTTP error! status: ${response.status}`);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
const data = await response.json();
|
|
386
|
+
|
|
387
|
+
const kvPairs = data.map(item => ({
|
|
388
|
+
key: item.Key,
|
|
389
|
+
value: item.Value ? Buffer.from(item.Value, 'base64').toString('utf-8') : null,
|
|
390
|
+
createIndex: item.CreateIndex,
|
|
391
|
+
modifyIndex: item.ModifyIndex,
|
|
392
|
+
flags: item.Flags
|
|
393
|
+
}));
|
|
394
|
+
|
|
395
|
+
return {
|
|
396
|
+
content: [
|
|
397
|
+
{
|
|
398
|
+
type: "text",
|
|
399
|
+
text: `Found ${kvPairs.length} key-value pair(s)${prefix ? ` with prefix "${prefix}"` : ''}:\n\n${JSON.stringify(kvPairs, null, 2)}`,
|
|
400
|
+
},
|
|
401
|
+
],
|
|
402
|
+
};
|
|
403
|
+
} catch (error) {
|
|
404
|
+
console.error("❌ Error getting keys recursively:", error);
|
|
405
|
+
return {
|
|
406
|
+
content: [
|
|
407
|
+
{
|
|
408
|
+
type: "text",
|
|
409
|
+
text: `Error getting keys recursively: ${error.message}`,
|
|
410
|
+
},
|
|
411
|
+
],
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
);
|
|
416
|
+
|
|
417
|
+
registerQConsulTool([
|
|
418
|
+
"search_keys",
|
|
419
|
+
"qconsul_search_keys"
|
|
420
|
+
],
|
|
421
|
+
{
|
|
422
|
+
title: "Search Consul KV Keys",
|
|
423
|
+
description: "Search for keys matching a pattern in Consul KV store",
|
|
424
|
+
inputSchema: z.object({
|
|
425
|
+
searchTerm: z.string().describe("Search term to find in key names"),
|
|
426
|
+
prefix: z.string().optional().default("").describe("Optional prefix to narrow search scope"),
|
|
427
|
+
datacenter: z.string().optional().describe("Datacenter to query (optional)")
|
|
428
|
+
}),
|
|
429
|
+
},
|
|
430
|
+
async ({ searchTerm, prefix, datacenter }) => {
|
|
431
|
+
console.error(`🔍 Searching for keys matching: ${searchTerm}`);
|
|
432
|
+
|
|
433
|
+
try {
|
|
434
|
+
const dc = datacenter || CONSUL_DATACENTER;
|
|
435
|
+
const url = `${CONSUL_URL}/v1/kv/${prefix}?dc=${dc}&keys=true`;
|
|
436
|
+
|
|
437
|
+
const response = await makeAuthenticatedRequest(url);
|
|
438
|
+
|
|
439
|
+
if (response.status === 404) {
|
|
440
|
+
return {
|
|
441
|
+
content: [
|
|
442
|
+
{
|
|
443
|
+
type: "text",
|
|
444
|
+
text: `No keys found`,
|
|
445
|
+
},
|
|
446
|
+
],
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
if (!response.ok) {
|
|
451
|
+
throw new Error(`HTTP error! status: ${response.status}`);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
const allKeys = await response.json();
|
|
455
|
+
|
|
456
|
+
const searchLower = searchTerm.toLowerCase();
|
|
457
|
+
const matchingKeys = allKeys.filter(key =>
|
|
458
|
+
key.toLowerCase().includes(searchLower)
|
|
459
|
+
);
|
|
460
|
+
|
|
461
|
+
if (matchingKeys.length === 0) {
|
|
462
|
+
return {
|
|
463
|
+
content: [
|
|
464
|
+
{
|
|
465
|
+
type: "text",
|
|
466
|
+
text: `No keys found matching "${searchTerm}"`,
|
|
467
|
+
},
|
|
468
|
+
],
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
return {
|
|
473
|
+
content: [
|
|
474
|
+
{
|
|
475
|
+
type: "text",
|
|
476
|
+
text: `Found ${matchingKeys.length} key(s) matching "${searchTerm}":\n\n${JSON.stringify(matchingKeys, null, 2)}`,
|
|
477
|
+
},
|
|
478
|
+
],
|
|
479
|
+
};
|
|
480
|
+
} catch (error) {
|
|
481
|
+
console.error("❌ Error searching keys:", error);
|
|
482
|
+
return {
|
|
483
|
+
content: [
|
|
484
|
+
{
|
|
485
|
+
type: "text",
|
|
486
|
+
text: `Error searching keys: ${error.message}`,
|
|
487
|
+
},
|
|
488
|
+
],
|
|
489
|
+
};
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
);
|
|
493
|
+
|
|
494
|
+
console.error("🚀 Starting QConsul MCP Server");
|
|
495
|
+
console.error(`🚀 Consul URL: ${CONSUL_URL}`);
|
|
496
|
+
console.error(`🔐 Authenticated as: ${CONSUL_USERNAME}`);
|
|
497
|
+
console.error(`🌍 Default Datacenter: ${CONSUL_DATACENTER}`);
|
|
498
|
+
|
|
499
|
+
const transport = new StdioServerTransport();
|
|
500
|
+
await server.connect(transport);
|