Initial commit: Caddy MCP Server
This commit is contained in:
+54
@@ -0,0 +1,54 @@
|
|||||||
|
# Compiled binaries
|
||||||
|
caddy-mcp
|
||||||
|
*.exe
|
||||||
|
*.exe~
|
||||||
|
*.dll
|
||||||
|
*.so
|
||||||
|
*.dylib
|
||||||
|
*.test
|
||||||
|
*.out
|
||||||
|
|
||||||
|
# Build artifacts
|
||||||
|
dist/
|
||||||
|
build/
|
||||||
|
bin/
|
||||||
|
release/
|
||||||
|
|
||||||
|
# Go specific
|
||||||
|
*.o
|
||||||
|
*.a
|
||||||
|
*.log
|
||||||
|
vendor/
|
||||||
|
Godeps/
|
||||||
|
go.work
|
||||||
|
go.work.sum
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
# Test artifacts
|
||||||
|
*.tmp
|
||||||
|
tmp/
|
||||||
|
temp/
|
||||||
|
coverage.out
|
||||||
|
coverage.html
|
||||||
|
*.cover
|
||||||
|
|
||||||
|
# Environment files
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
.env.*.local
|
||||||
|
*.env
|
||||||
|
|
||||||
|
# Debug
|
||||||
|
debug/
|
||||||
|
__debug_bin
|
||||||
|
|
||||||
|
# OS
|
||||||
|
Thumbs.db
|
||||||
|
.sisyphus/
|
||||||
@@ -0,0 +1,190 @@
|
|||||||
|
Apache License
|
||||||
|
Version 2.0, January 2004
|
||||||
|
http://www.apache.org/licenses/
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||||
|
|
||||||
|
1. Definitions.
|
||||||
|
|
||||||
|
"License" shall mean the terms and conditions for use, reproduction,
|
||||||
|
and distribution as defined by Sections 1 through 9 of this document.
|
||||||
|
|
||||||
|
"Licensor" shall mean the copyright owner or entity authorized by
|
||||||
|
the copyright owner that is granting the License.
|
||||||
|
|
||||||
|
"Legal Entity" shall mean the union of the acting entity and all
|
||||||
|
other entities that control, are controlled by, or are under common
|
||||||
|
control with that entity. For the purposes of this definition,
|
||||||
|
"control" means (i) the power, direct or indirect, to cause the
|
||||||
|
direction or management of such entity, whether by contract or
|
||||||
|
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||||
|
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||||
|
|
||||||
|
"You" (or "Your") shall mean an individual or Legal Entity
|
||||||
|
exercising permissions granted by this License.
|
||||||
|
|
||||||
|
"Source" form shall mean the preferred form for making modifications,
|
||||||
|
including but not limited to software source code, documentation
|
||||||
|
source, and configuration files.
|
||||||
|
|
||||||
|
"Object" form shall mean any form resulting from mechanical
|
||||||
|
transformation or translation of a Source form, including but
|
||||||
|
not limited to compiled object code, generated documentation,
|
||||||
|
and conversions to other media types.
|
||||||
|
|
||||||
|
"Work" shall mean the work of authorship, whether in Source or
|
||||||
|
Object form, made available under the License, as indicated by a
|
||||||
|
copyright notice that is included in or attached to the work
|
||||||
|
(an example is provided in the Appendix below).
|
||||||
|
|
||||||
|
"Derivative Works" shall mean any work, whether in Source or Object
|
||||||
|
form, that is based on (or derived from) the Work and for which the
|
||||||
|
editorial revisions, annotations, elaborations, or other modifications
|
||||||
|
represent, as a whole, an original work of authorship. For the purposes
|
||||||
|
of this License, Derivative Works shall not include works that remain
|
||||||
|
separable from, or merely link (or bind by name) to the interfaces of,
|
||||||
|
the Work and Derivative Works thereof.
|
||||||
|
|
||||||
|
"Contribution" shall mean any work of authorship, including
|
||||||
|
the original version of the Work and any modifications or additions
|
||||||
|
to that Work or Derivative Works thereof, that is intentionally
|
||||||
|
submitted to the Licensor for inclusion in the Work by the copyright owner
|
||||||
|
or by an individual or Legal Entity authorized to submit on behalf of
|
||||||
|
the copyright owner. For the purposes of this definition, "submitted"
|
||||||
|
means any form of electronic, verbal, or written communication sent
|
||||||
|
to the Licensor or its representatives, including but not limited to
|
||||||
|
communication on electronic mailing lists, source code control systems,
|
||||||
|
and issue tracking systems that are managed by, or on behalf of, the
|
||||||
|
Licensor for the purpose of discussing and improving the Work, but
|
||||||
|
excluding communication that is conspicuously marked or otherwise
|
||||||
|
designated in writing by the copyright owner as "Not a Contribution."
|
||||||
|
|
||||||
|
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||||
|
on behalf of whom a Contribution has been received by Licensor and
|
||||||
|
subsequently incorporated within the Work.
|
||||||
|
|
||||||
|
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
copyright license to reproduce, prepare Derivative Works of,
|
||||||
|
publicly display, publicly perform, sublicense, and distribute the
|
||||||
|
Work and such Derivative Works in Source or Object form.
|
||||||
|
|
||||||
|
3. Grant of Patent License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
(except as stated in this section) patent license to make, have made,
|
||||||
|
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||||
|
where such license license applies only to those patent claims licensable
|
||||||
|
by such Contributor that are necessarily infringed by their
|
||||||
|
Contribution(s) alone or by combination of their Contribution(s)
|
||||||
|
with the Work to which such Contribution(s) was submitted. If You
|
||||||
|
institute patent litigation against any entity (including a
|
||||||
|
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||||
|
or a Contribution incorporated within the Work constitutes direct
|
||||||
|
or contributory patent infringement, then any patent licenses
|
||||||
|
granted to You under this License for that Work shall terminate
|
||||||
|
as of the date such litigation is filed.
|
||||||
|
|
||||||
|
4. Redistribution. You may reproduce and distribute copies of the
|
||||||
|
Work or Derivative Works thereof in any medium, with or without
|
||||||
|
modifications, and in Source or Object form, provided that You
|
||||||
|
meet the following conditions:
|
||||||
|
|
||||||
|
(a) You must give any other recipients of the Work or
|
||||||
|
Derivative Works a copy of this License; and
|
||||||
|
|
||||||
|
(b) You must cause any modified files to carry prominent notices
|
||||||
|
stating that You changed the files; and
|
||||||
|
|
||||||
|
(c) You must retain, in the Source form of any Derivative Works
|
||||||
|
that You distribute, all copyright, patent, trademark, and
|
||||||
|
attribution notices from the Source form of the Work,
|
||||||
|
excluding those notices that do not pertain to any part of
|
||||||
|
the Derivative Works; and
|
||||||
|
|
||||||
|
(d) If the Work includes a "NOTICE" text file as part of its
|
||||||
|
distribution, then any Derivative Works that You distribute must
|
||||||
|
include a readable copy of the attribution notices contained
|
||||||
|
within such NOTICE file, excluding those notices that do not
|
||||||
|
pertain to any part of the Derivative Works, in at least one
|
||||||
|
of the following places: within a NOTICE text file distributed
|
||||||
|
as part of the Derivative Works; within the Source form or
|
||||||
|
documentation, if provided along with the Derivative Works; or,
|
||||||
|
within a display generated by the Derivative Works, if and
|
||||||
|
wherever such third-party notices normally appear. The contents
|
||||||
|
of the NOTICE file are for informational purposes only and
|
||||||
|
do not modify the License. You may add Your own attribution
|
||||||
|
notices within Derivative Works that You distribute, alongside
|
||||||
|
or as an addendum to the NOTICE text from the Work, provided
|
||||||
|
that such additional attribution notices cannot be construed
|
||||||
|
as modifying the License.
|
||||||
|
|
||||||
|
You may add Your own copyright statement to Your modifications and
|
||||||
|
may provide additional or different license terms and conditions
|
||||||
|
for use, reproduction, or distribution of Your modifications, or
|
||||||
|
for any such Derivative Works as a whole, provided Your use,
|
||||||
|
reproduction, and distribution of the Work otherwise complies with
|
||||||
|
the conditions stated in this License.
|
||||||
|
|
||||||
|
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||||
|
any Contribution intentionally submitted for inclusion in the Work
|
||||||
|
by You to the Licensor shall be under the terms and conditions of
|
||||||
|
this License, without any additional terms or conditions.
|
||||||
|
Notwithstanding the above, nothing herein shall supersede or modify
|
||||||
|
the terms of any separate license agreement you may have executed
|
||||||
|
with Licensor regarding such Contributions.
|
||||||
|
|
||||||
|
6. Trademarks. This License does not grant permission to use the trade
|
||||||
|
names, trademarks, service marks, or product names of the Licensor,
|
||||||
|
except as required for reasonable and customary use in describing the
|
||||||
|
origin of the Work and reproducing the content of the NOTICE file.
|
||||||
|
|
||||||
|
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||||
|
agreed to in writing, Licensor provides the Work (and each
|
||||||
|
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
|
implied, including, without limitation, any warranties or conditions
|
||||||
|
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||||
|
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||||
|
appropriateness of using or redistributing the Work and assume any
|
||||||
|
risks associated with Your exercise of permissions under this License.
|
||||||
|
|
||||||
|
8. Limitation of Liability. In no event and under no legal theory,
|
||||||
|
whether in tort (including negligence), contract, or otherwise,
|
||||||
|
unless required by applicable law (such as deliberate and grossly
|
||||||
|
negligent acts) or agreed to in writing, shall any Contributor be
|
||||||
|
liable to You for damages, including any direct, indirect, special,
|
||||||
|
incidental, or consequential damages of any character arising as a
|
||||||
|
result of this License or out of the use or inability to use the
|
||||||
|
Work (including but not limited to damages for loss of goodwill,
|
||||||
|
work stoppage, computer failure or malfunction, or any and all
|
||||||
|
other commercial damages or losses), even if such Contributor
|
||||||
|
has been advised of the possibility of such damages.
|
||||||
|
|
||||||
|
9. Accepting Warranty or Additional Liability. While redistributing
|
||||||
|
the Work or Derivative Works thereof, You may choose to offer,
|
||||||
|
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||||
|
or other liability obligations and/or rights consistent with this
|
||||||
|
License. However, in accepting such obligations, You may act only
|
||||||
|
on Your own behalf and on Your sole responsibility, not on behalf
|
||||||
|
of any other Contributor, and only if You agree to indemnify,
|
||||||
|
defend, and hold each Contributor harmless for any liability
|
||||||
|
incurred by, or claims asserted against, such Contributor by reason
|
||||||
|
of your accepting any such warranty or additional liability.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
Copyright 2026 Lumbridge Corporation
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
@@ -0,0 +1,117 @@
|
|||||||
|
# Caddy MCP Server
|
||||||
|
|
||||||
|
A comprehensive Model Context Protocol (MCP) server for managing Caddy web server via the Admin API.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
Complete Caddy management with 21 powerful tools:
|
||||||
|
|
||||||
|
### Site Management
|
||||||
|
| Tool | Description |
|
||||||
|
|------|-------------|
|
||||||
|
| `list_sites` | List all configured sites with upstreams & auth status |
|
||||||
|
| `get_site` | Get detailed configuration for a specific site |
|
||||||
|
| `get_site_routes` | Get all routes (including path-based) for a site |
|
||||||
|
| `add_site` | Add a new site with reverse proxy |
|
||||||
|
| `remove_site` | Remove a site configuration |
|
||||||
|
| `update_upstream` | Update the upstream address for a site |
|
||||||
|
|
||||||
|
### Path-Based Routing
|
||||||
|
| Tool | Description |
|
||||||
|
|------|-------------|
|
||||||
|
| `add_path_route` | Add path-based route (e.g., `/api/*` → localhost:3000) |
|
||||||
|
| `remove_path_route` | Remove a path-based route |
|
||||||
|
|
||||||
|
### Authentication
|
||||||
|
| Tool | Description |
|
||||||
|
|------|-------------|
|
||||||
|
| `add_basic_auth` | Add basic authentication to a site |
|
||||||
|
| `remove_basic_auth` | Remove basic authentication |
|
||||||
|
|
||||||
|
### Headers
|
||||||
|
| Tool | Description |
|
||||||
|
|------|-------------|
|
||||||
|
| `add_request_header` | Add header sent to upstream |
|
||||||
|
| `add_response_header` | Add header sent to client |
|
||||||
|
|
||||||
|
### File Server
|
||||||
|
| Tool | Description |
|
||||||
|
|------|-------------|
|
||||||
|
| `add_file_server` | Add static file server |
|
||||||
|
| `update_file_root` | Update file server root directory |
|
||||||
|
|
||||||
|
### Review Environments
|
||||||
|
| Tool | Description |
|
||||||
|
|------|-------------|
|
||||||
|
| `add_wip_environment` | Add WIP review env to review.example.com |
|
||||||
|
|
||||||
|
### Configuration
|
||||||
|
| Tool | Description |
|
||||||
|
|------|-------------|
|
||||||
|
| `export_config` | Export full Caddy JSON config |
|
||||||
|
| `adapt_caddyfile` | Convert Caddyfile to JSON |
|
||||||
|
| `validate_config` | Validate config without applying |
|
||||||
|
|
||||||
|
### Server Control
|
||||||
|
| Tool | Description |
|
||||||
|
|------|-------------|
|
||||||
|
| `caddy_status` | Check Caddy health |
|
||||||
|
| `stop_caddy` | Gracefully stop Caddy |
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Build
|
||||||
|
go build -o caddy-mcp .
|
||||||
|
|
||||||
|
# Run locally
|
||||||
|
./caddy-mcp
|
||||||
|
|
||||||
|
# Run against remote Caddy (via SSH tunnel)
|
||||||
|
ssh -L 2019:localhost:2019 server-1 &
|
||||||
|
./caddy-mcp
|
||||||
|
```
|
||||||
|
|
||||||
|
## Environment
|
||||||
|
|
||||||
|
- `CADDY_HOST` - Caddy admin API host (default: `localhost:2019`)
|
||||||
|
- `REVIEW_DOMAIN` - Domain for WIP review environments (default: `review.example.com`)
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
- `client.go` - Caddy Admin API client
|
||||||
|
- `server.go` - MCP server setup
|
||||||
|
- `handlers.go` - Tool implementations
|
||||||
|
- `site_ops.go` - Site read operations
|
||||||
|
- `site_modify.go` - Site write operations
|
||||||
|
- `path_routes.go` - Path-based routing
|
||||||
|
- `auth_ops.go` - Authentication management
|
||||||
|
- `headers.go` - Header manipulation
|
||||||
|
- `fileserver.go` - Static file serving
|
||||||
|
|
||||||
|
## Security
|
||||||
|
|
||||||
|
### Password Transmission
|
||||||
|
**WARNING**: Passwords are transmitted in plaintext from the MCP client to this server.
|
||||||
|
This is an architectural limitation of the Model Context Protocol (MCP).
|
||||||
|
While Caddy stores passwords hashed (bcrypt), the password travels unencrypted
|
||||||
|
through the MCP communication channel.
|
||||||
|
|
||||||
|
### Caddy Admin API
|
||||||
|
The Caddy admin API (`localhost:2019` by default) uses HTTP, not HTTPS.
|
||||||
|
When running this MCP server remotely, use SSH port forwarding to secure the connection:
|
||||||
|
```bash
|
||||||
|
ssh -L 2019:localhost:2019 <remote-server> &
|
||||||
|
./caddy-mcp
|
||||||
|
```
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# List all sites
|
||||||
|
./caddy-mcp
|
||||||
|
# Send: {"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"list_sites","arguments":{}}}
|
||||||
|
|
||||||
|
# Add a WIP environment
|
||||||
|
# {"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"add_wip_environment","arguments":{"project":"my-project","port":3000}}}
|
||||||
|
```
|
||||||
+171
@@ -0,0 +1,171 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (c *CaddyClient) AddBasicAuth(domain, username, password string) error {
|
||||||
|
log.Printf("[WARNING] Password for user %s on domain %s is being transmitted in plaintext through MCP", username, domain)
|
||||||
|
config, err := c.GetConfig()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
apps, _ := config["apps"].(map[string]interface{})
|
||||||
|
httpApps, _ := apps["http"].(map[string]interface{})
|
||||||
|
servers, _ := httpApps["servers"].(map[string]interface{})
|
||||||
|
|
||||||
|
for srvName, server := range servers {
|
||||||
|
srv, _ := server.(map[string]interface{})
|
||||||
|
routes, _ := srv["routes"].([]interface{})
|
||||||
|
|
||||||
|
for routeIdx, route := range routes {
|
||||||
|
routeMap, _ := route.(map[string]interface{})
|
||||||
|
matches, _ := routeMap["match"].([]interface{})
|
||||||
|
|
||||||
|
for _, match := range matches {
|
||||||
|
matchMap, _ := match.(map[string]interface{})
|
||||||
|
hosts, _ := matchMap["host"].([]interface{})
|
||||||
|
if len(hosts) > 0 {
|
||||||
|
d, _ := hosts[0].(string)
|
||||||
|
if d == domain {
|
||||||
|
handle, _ := routeMap["handle"].([]interface{})
|
||||||
|
for i, h := range handle {
|
||||||
|
handlerMap, ok := h.(map[string]interface{})
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if handlerMap["handler"] == "subroute" {
|
||||||
|
subRoutes, _ := handlerMap["routes"].([]interface{})
|
||||||
|
if len(subRoutes) > 0 {
|
||||||
|
mainRoute, _ := subRoutes[0].(map[string]interface{})
|
||||||
|
handlers, _ := mainRoute["handle"].([]interface{})
|
||||||
|
|
||||||
|
// Check if auth already exists
|
||||||
|
var authIdx = -1
|
||||||
|
for idx, handler := range handlers {
|
||||||
|
hm, _ := handler.(map[string]interface{})
|
||||||
|
if hm["handler"] == "authentication" {
|
||||||
|
authIdx = idx
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WARNING: Passwords are transmitted in plaintext through MCP protocol.
|
||||||
|
// This is an architectural limitation of the Model Context Protocol.
|
||||||
|
// The password is stored hashed in Caddy (bcrypt), but travels
|
||||||
|
// unencrypted from the MCP client to this server.
|
||||||
|
newAccount := map[string]interface{}{
|
||||||
|
"username": username,
|
||||||
|
"password": password,
|
||||||
|
}
|
||||||
|
|
||||||
|
if authIdx >= 0 {
|
||||||
|
// Add to existing auth
|
||||||
|
authHandler, _ := handlers[authIdx].(map[string]interface{})
|
||||||
|
providers, _ := authHandler["providers"].(map[string]interface{})
|
||||||
|
httpBasic, _ := providers["http_basic"].(map[string]interface{})
|
||||||
|
accounts, _ := httpBasic["accounts"].([]interface{})
|
||||||
|
accounts = append(accounts, newAccount)
|
||||||
|
httpBasic["accounts"] = accounts
|
||||||
|
providers["http_basic"] = httpBasic
|
||||||
|
authHandler["providers"] = providers
|
||||||
|
handlers[authIdx] = authHandler
|
||||||
|
} else {
|
||||||
|
// Create new auth handler
|
||||||
|
authHandler := map[string]interface{}{
|
||||||
|
"handler": "authentication",
|
||||||
|
"providers": map[string]interface{}{
|
||||||
|
"http_basic": map[string]interface{}{
|
||||||
|
"accounts": []interface{}{newAccount},
|
||||||
|
"hash": map[string]interface{}{
|
||||||
|
"algorithm": "bcrypt",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
handlers = append([]interface{}{authHandler}, handlers...)
|
||||||
|
}
|
||||||
|
|
||||||
|
mainRoute["handle"] = handlers
|
||||||
|
subRoutes[0] = mainRoute
|
||||||
|
handlerMap["routes"] = subRoutes
|
||||||
|
handle[i] = handlerMap
|
||||||
|
routeMap["handle"] = handle
|
||||||
|
routes[routeIdx] = routeMap
|
||||||
|
|
||||||
|
configPath := fmt.Sprintf("apps/http/servers/%s/routes", srvName)
|
||||||
|
return c.SetConfig(configPath, routes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fmt.Errorf("site not found: %s", domain)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CaddyClient) RemoveBasicAuth(domain string) error {
|
||||||
|
config, err := c.GetConfig()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
apps, _ := config["apps"].(map[string]interface{})
|
||||||
|
httpApps, _ := apps["http"].(map[string]interface{})
|
||||||
|
servers, _ := httpApps["servers"].(map[string]interface{})
|
||||||
|
|
||||||
|
for srvName, server := range servers {
|
||||||
|
srv, _ := server.(map[string]interface{})
|
||||||
|
routes, _ := srv["routes"].([]interface{})
|
||||||
|
|
||||||
|
for routeIdx, route := range routes {
|
||||||
|
routeMap, _ := route.(map[string]interface{})
|
||||||
|
matches, _ := routeMap["match"].([]interface{})
|
||||||
|
|
||||||
|
for _, match := range matches {
|
||||||
|
matchMap, _ := match.(map[string]interface{})
|
||||||
|
hosts, _ := matchMap["host"].([]interface{})
|
||||||
|
if len(hosts) > 0 {
|
||||||
|
d, _ := hosts[0].(string)
|
||||||
|
if d == domain {
|
||||||
|
handle, _ := routeMap["handle"].([]interface{})
|
||||||
|
for i, h := range handle {
|
||||||
|
handlerMap, ok := h.(map[string]interface{})
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if handlerMap["handler"] == "subroute" {
|
||||||
|
subRoutes, _ := handlerMap["routes"].([]interface{})
|
||||||
|
if len(subRoutes) > 0 {
|
||||||
|
mainRoute, _ := subRoutes[0].(map[string]interface{})
|
||||||
|
handlers, _ := mainRoute["handle"].([]interface{})
|
||||||
|
var newHandlers []interface{}
|
||||||
|
for _, handler := range handlers {
|
||||||
|
hm, _ := handler.(map[string]interface{})
|
||||||
|
if hm["handler"] != "authentication" {
|
||||||
|
newHandlers = append(newHandlers, handler)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mainRoute["handle"] = newHandlers
|
||||||
|
subRoutes[0] = mainRoute
|
||||||
|
handlerMap["routes"] = subRoutes
|
||||||
|
handle[i] = handlerMap
|
||||||
|
routeMap["handle"] = handle
|
||||||
|
routes[routeIdx] = routeMap
|
||||||
|
|
||||||
|
configPath := fmt.Sprintf("apps/http/servers/%s/routes", srvName)
|
||||||
|
return c.SetConfig(configPath, routes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fmt.Errorf("site not found: %s", domain)
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
cd "$(dirname "$0")"
|
||||||
|
|
||||||
|
# Initialize go module if needed
|
||||||
|
if [ ! -f go.sum ]; then
|
||||||
|
echo "📦 Initializing Go module..."
|
||||||
|
go mod tidy
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Build
|
||||||
|
echo "🔨 Building caddy-mcp..."
|
||||||
|
go build -o caddy-mcp .
|
||||||
|
|
||||||
|
echo "✅ Build complete: ./caddy-mcp"
|
||||||
|
echo ""
|
||||||
|
echo "Usage:"
|
||||||
|
echo " ./caddy-mcp"
|
||||||
Executable
BIN
Binary file not shown.
@@ -0,0 +1,172 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CaddyClient struct {
|
||||||
|
baseURL string
|
||||||
|
client *http.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewCaddyClient(host string) *CaddyClient {
|
||||||
|
if host == "" {
|
||||||
|
host = "localhost:2019"
|
||||||
|
}
|
||||||
|
return &CaddyClient{
|
||||||
|
baseURL: "http://" + host,
|
||||||
|
client: &http.Client{Timeout: 30 * time.Second},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CaddyClient) doRequest(method, path string, body []byte) (*http.Response, error) {
|
||||||
|
var bodyReader io.Reader
|
||||||
|
if body != nil {
|
||||||
|
bodyReader = bytes.NewReader(body)
|
||||||
|
}
|
||||||
|
req, err := http.NewRequest(method, c.baseURL+path, bodyReader)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if body != nil {
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
}
|
||||||
|
return c.client.Do(req)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CaddyClient) GetConfig() (map[string]interface{}, error) {
|
||||||
|
resp, err := c.doRequest("GET", "/config/", nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
if resp.StatusCode != 200 {
|
||||||
|
body, _ := io.ReadAll(resp.Body)
|
||||||
|
return nil, fmt.Errorf("API error %d: %s", resp.StatusCode, string(body))
|
||||||
|
}
|
||||||
|
var config map[string]interface{}
|
||||||
|
if err := json.NewDecoder(resp.Body).Decode(&config); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return config, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CaddyClient) SetConfig(path string, value interface{}) error {
|
||||||
|
data, _ := json.Marshal(value)
|
||||||
|
resp, err := c.doRequest("PUT", "/config/"+path, data)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
if resp.StatusCode != 200 {
|
||||||
|
body, _ := io.ReadAll(resp.Body)
|
||||||
|
return fmt.Errorf("API error %d: %s", resp.StatusCode, string(body))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CaddyClient) DeleteConfig(path string) error {
|
||||||
|
resp, err := c.doRequest("DELETE", "/config/"+path, nil)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
if resp.StatusCode != 200 {
|
||||||
|
body, _ := io.ReadAll(resp.Body)
|
||||||
|
return fmt.Errorf("API error %d: %s", resp.StatusCode, string(body))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CaddyClient) PostConfig(path string, value interface{}) error {
|
||||||
|
data, _ := json.Marshal(value)
|
||||||
|
resp, err := c.doRequest("POST", "/config/"+path, data)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
if resp.StatusCode != 200 {
|
||||||
|
body, _ := io.ReadAll(resp.Body)
|
||||||
|
return fmt.Errorf("API error %d: %s", resp.StatusCode, string(body))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CaddyClient) LoadConfig(config interface{}) error {
|
||||||
|
data, _ := json.Marshal(config)
|
||||||
|
resp, err := c.doRequest("POST", "/load", data)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
if resp.StatusCode != 200 {
|
||||||
|
body, _ := io.ReadAll(resp.Body)
|
||||||
|
return fmt.Errorf("API error %d: %s", resp.StatusCode, string(body))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CaddyClient) Stop() error {
|
||||||
|
resp, err := c.doRequest("GET", "/stop", nil)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CaddyClient) GetConfigAtPath(path string) (interface{}, error) {
|
||||||
|
resp, err := c.doRequest("GET", "/config/"+path, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
if resp.StatusCode == 400 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
if resp.StatusCode != 200 {
|
||||||
|
body, _ := io.ReadAll(resp.Body)
|
||||||
|
return nil, fmt.Errorf("API error %d: %s", resp.StatusCode, string(body))
|
||||||
|
}
|
||||||
|
var result interface{}
|
||||||
|
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CaddyClient) AdaptCaddyfile(caddyfile string) (map[string]interface{}, error) {
|
||||||
|
resp, err := c.doRequest("POST", "/adapt", []byte(caddyfile))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
if resp.StatusCode != 200 {
|
||||||
|
body, _ := io.ReadAll(resp.Body)
|
||||||
|
return nil, fmt.Errorf("adapt error %d: %s", resp.StatusCode, string(body))
|
||||||
|
}
|
||||||
|
var result map[string]interface{}
|
||||||
|
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CaddyClient) ValidateConfig(config map[string]interface{}) error {
|
||||||
|
data, _ := json.Marshal(config)
|
||||||
|
resp, err := c.doRequest("POST", "/validate", data)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
if resp.StatusCode != 200 {
|
||||||
|
body, _ := io.ReadAll(resp.Body)
|
||||||
|
return fmt.Errorf("validation failed: %s", string(body))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
func (c *CaddyClient) AddFileServer(domain, root string, browse bool) error {
|
||||||
|
route := map[string]interface{}{
|
||||||
|
"handle": []interface{}{
|
||||||
|
map[string]interface{}{
|
||||||
|
"handler": "vars",
|
||||||
|
"root": root,
|
||||||
|
},
|
||||||
|
map[string]interface{}{
|
||||||
|
"handler": "file_server",
|
||||||
|
"browse": browse,
|
||||||
|
"hide": []string{".git", ".env"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"match": []interface{}{map[string]interface{}{"host": []string{domain}}},
|
||||||
|
"terminal": true,
|
||||||
|
}
|
||||||
|
|
||||||
|
config, _ := c.GetConfig()
|
||||||
|
apps, _ := config["apps"].(map[string]interface{})
|
||||||
|
httpApps, _ := apps["http"].(map[string]interface{})
|
||||||
|
servers, _ := httpApps["servers"].(map[string]interface{})
|
||||||
|
|
||||||
|
for srvName := range servers {
|
||||||
|
routesPath := fmt.Sprintf("apps/http/servers/%s/routes", srvName)
|
||||||
|
routes, _ := c.GetConfigAtPath(routesPath)
|
||||||
|
if routes != nil {
|
||||||
|
routesList, _ := routes.([]interface{})
|
||||||
|
routesList = append(routesList, route)
|
||||||
|
return c.SetConfig(routesPath, routesList)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.SetConfig("apps/http/servers/srv0/routes", []interface{}{route})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CaddyClient) UpdateFileServerRoot(domain, root string) error {
|
||||||
|
config, err := c.GetConfig()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
apps, _ := config["apps"].(map[string]interface{})
|
||||||
|
httpApps, _ := apps["http"].(map[string]interface{})
|
||||||
|
servers, _ := httpApps["servers"].(map[string]interface{})
|
||||||
|
|
||||||
|
for srvName, server := range servers {
|
||||||
|
srv, _ := server.(map[string]interface{})
|
||||||
|
routes, _ := srv["routes"].([]interface{})
|
||||||
|
|
||||||
|
for routeIdx, route := range routes {
|
||||||
|
routeMap, _ := route.(map[string]interface{})
|
||||||
|
matches, _ := routeMap["match"].([]interface{})
|
||||||
|
|
||||||
|
for _, match := range matches {
|
||||||
|
matchMap, _ := match.(map[string]interface{})
|
||||||
|
hosts, _ := matchMap["host"].([]interface{})
|
||||||
|
if len(hosts) > 0 {
|
||||||
|
d, _ := hosts[0].(string)
|
||||||
|
if d == domain {
|
||||||
|
handle, _ := routeMap["handle"].([]interface{})
|
||||||
|
for i, h := range handle {
|
||||||
|
handlerMap, ok := h.(map[string]interface{})
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if handlerMap["handler"] == "vars" {
|
||||||
|
handlerMap["root"] = root
|
||||||
|
handle[i] = handlerMap
|
||||||
|
routeMap["handle"] = handle
|
||||||
|
routes[routeIdx] = routeMap
|
||||||
|
|
||||||
|
configPath := fmt.Sprintf("apps/http/servers/%s/routes", srvName)
|
||||||
|
return c.SetConfig(configPath, routes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fmt.Errorf("file server not found for: %s", domain)
|
||||||
|
}
|
||||||
+305
@@ -0,0 +1,305 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Site Management Handlers
|
||||||
|
func (s *MCPServer) handleListSites(id interface{}) {
|
||||||
|
sites, err := s.client.ListSites()
|
||||||
|
if err != nil {
|
||||||
|
s.sendError(id, -32000, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var output string
|
||||||
|
for _, site := range sites {
|
||||||
|
domain, _ := site["domain"].(string)
|
||||||
|
upstreams := s.client.extractUpstreams(site["route"].(map[string]interface{}))
|
||||||
|
output += fmt.Sprintf("📍 %s\n", domain)
|
||||||
|
if len(upstreams) > 0 {
|
||||||
|
output += fmt.Sprintf(" → %v\n", upstreams)
|
||||||
|
}
|
||||||
|
if s.client.hasBasicAuth(site["route"].(map[string]interface{})) {
|
||||||
|
output += " 🔒 Auth enabled\n"
|
||||||
|
}
|
||||||
|
output += "\n"
|
||||||
|
}
|
||||||
|
if len(sites) == 0 {
|
||||||
|
output = "No sites configured\n"
|
||||||
|
}
|
||||||
|
s.sendTextResult(id, output)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *MCPServer) handleGetSite(id interface{}, args map[string]interface{}) {
|
||||||
|
domain, _ := args["domain"].(string)
|
||||||
|
site, err := s.client.GetSite(domain)
|
||||||
|
if err != nil {
|
||||||
|
s.sendError(id, -32000, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
output := fmt.Sprintf("Domain: %s\n", domain)
|
||||||
|
output += fmt.Sprintf("Server: %s\n", site["server"])
|
||||||
|
output += fmt.Sprintf("Upstreams: %v\n", site["upstreams"])
|
||||||
|
output += fmt.Sprintf("Auth: %v\n", site["has_auth"])
|
||||||
|
handlers, _ := json.MarshalIndent(site["handlers"], "", " ")
|
||||||
|
output += fmt.Sprintf("Handlers: %s\n", string(handlers))
|
||||||
|
s.sendTextResult(id, output)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *MCPServer) handleAddSite(id interface{}, args map[string]interface{}) {
|
||||||
|
domain, _ := args["domain"].(string)
|
||||||
|
if err := validateDomain(domain); err != nil {
|
||||||
|
s.sendError(id, -32000, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
upstream, _ := args["upstream"].(string)
|
||||||
|
if err := s.client.AddSite(domain, upstream); err != nil {
|
||||||
|
s.sendError(id, -32000, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
s.sendTextResult(id, fmt.Sprintf("✅ Added %s → %s", domain, upstream))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *MCPServer) handleRemoveSite(id interface{}, args map[string]interface{}) {
|
||||||
|
domain, _ := args["domain"].(string)
|
||||||
|
if err := s.client.RemoveSite(domain); err != nil {
|
||||||
|
s.sendError(id, -32000, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
s.sendTextResult(id, fmt.Sprintf("✅ Removed %s", domain))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *MCPServer) handleUpdateUpstream(id interface{}, args map[string]interface{}) {
|
||||||
|
domain, _ := args["domain"].(string)
|
||||||
|
upstream, _ := args["upstream"].(string)
|
||||||
|
if err := s.client.UpdateUpstream(domain, upstream); err != nil {
|
||||||
|
s.sendError(id, -32000, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
s.sendTextResult(id, fmt.Sprintf("✅ Updated %s → %s", domain, upstream))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Path Route Handlers
|
||||||
|
func (s *MCPServer) handleAddPathRoute(id interface{}, args map[string]interface{}) {
|
||||||
|
domain, _ := args["domain"].(string)
|
||||||
|
if err := validateDomain(domain); err != nil {
|
||||||
|
s.sendError(id, -32000, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
path, _ := args["path"].(string)
|
||||||
|
if err := validatePath(path); err != nil {
|
||||||
|
s.sendError(id, -32000, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
upstream, _ := args["upstream"].(string)
|
||||||
|
if err := s.client.AddPathRoute(domain, path, upstream); err != nil {
|
||||||
|
s.sendError(id, -32000, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
s.sendTextResult(id, fmt.Sprintf("✅ Added %s%s/* → %s", domain, path, upstream))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *MCPServer) handleRemovePathRoute(id interface{}, args map[string]interface{}) {
|
||||||
|
domain, _ := args["domain"].(string)
|
||||||
|
if err := validateDomain(domain); err != nil {
|
||||||
|
s.sendError(id, -32000, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
path, _ := args["path"].(string)
|
||||||
|
if err := validatePath(path); err != nil {
|
||||||
|
s.sendError(id, -32000, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := s.client.RemovePathRoute(domain, path); err != nil {
|
||||||
|
s.sendError(id, -32000, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
s.sendTextResult(id, fmt.Sprintf("✅ Removed %s%s/*", domain, path))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auth Handlers
|
||||||
|
func (s *MCPServer) handleAddBasicAuth(id interface{}, args map[string]interface{}) {
|
||||||
|
domain, _ := args["domain"].(string)
|
||||||
|
username, _ := args["username"].(string)
|
||||||
|
password, _ := args["password"].(string)
|
||||||
|
if err := s.client.AddBasicAuth(domain, username, password); err != nil {
|
||||||
|
s.sendError(id, -32000, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
s.sendTextResult(id, fmt.Sprintf("✅ Added basic auth for %s (user: %s)", domain, username))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *MCPServer) handleRemoveBasicAuth(id interface{}, args map[string]interface{}) {
|
||||||
|
domain, _ := args["domain"].(string)
|
||||||
|
if err := s.client.RemoveBasicAuth(domain); err != nil {
|
||||||
|
s.sendError(id, -32000, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
s.sendTextResult(id, fmt.Sprintf("✅ Removed basic auth from %s", domain))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Header Handlers
|
||||||
|
func (s *MCPServer) handleAddRequestHeader(id interface{}, args map[string]interface{}) {
|
||||||
|
domain, _ := args["domain"].(string)
|
||||||
|
if err := validateDomain(domain); err != nil {
|
||||||
|
s.sendError(id, -32000, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
header, _ := args["header"].(string)
|
||||||
|
if err := validateHeaderName(header); err != nil {
|
||||||
|
s.sendError(id, -32000, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
value, _ := args["value"].(string)
|
||||||
|
if err := s.client.AddHeaderUp(domain, header, value); err != nil {
|
||||||
|
s.sendError(id, -32000, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
s.sendTextResult(id, fmt.Sprintf("✅ Added request header %s: %s for %s", header, value, domain))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *MCPServer) handleAddResponseHeader(id interface{}, args map[string]interface{}) {
|
||||||
|
domain, _ := args["domain"].(string)
|
||||||
|
if err := validateDomain(domain); err != nil {
|
||||||
|
s.sendError(id, -32000, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
header, _ := args["header"].(string)
|
||||||
|
if err := validateHeaderName(header); err != nil {
|
||||||
|
s.sendError(id, -32000, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
value, _ := args["value"].(string)
|
||||||
|
if err := s.client.AddHeaderDown(domain, header, value); err != nil {
|
||||||
|
s.sendError(id, -32000, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
s.sendTextResult(id, fmt.Sprintf("✅ Added response header %s: %s for %s", header, value, domain))
|
||||||
|
}
|
||||||
|
|
||||||
|
// File Server Handlers
|
||||||
|
func (s *MCPServer) handleAddFileServer(id interface{}, args map[string]interface{}) {
|
||||||
|
domain, _ := args["domain"].(string)
|
||||||
|
root, _ := args["root"].(string)
|
||||||
|
browse, _ := args["browse"].(bool)
|
||||||
|
if err := s.client.AddFileServer(domain, root, browse); err != nil {
|
||||||
|
s.sendError(id, -32000, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
s.sendTextResult(id, fmt.Sprintf("✅ Added file server %s → %s (browse: %v)", domain, root, browse))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *MCPServer) handleUpdateFileRoot(id interface{}, args map[string]interface{}) {
|
||||||
|
domain, _ := args["domain"].(string)
|
||||||
|
root, _ := args["root"].(string)
|
||||||
|
if err := s.client.UpdateFileServerRoot(domain, root); err != nil {
|
||||||
|
s.sendError(id, -32000, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
s.sendTextResult(id, fmt.Sprintf("✅ Updated %s root → %s", domain, root))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Review Environment Handler
|
||||||
|
func (s *MCPServer) handleAddWipEnvironment(id interface{}, args map[string]interface{}) {
|
||||||
|
project, _ := args["project"].(string)
|
||||||
|
portFloat, _ := args["port"].(float64)
|
||||||
|
port := int(portFloat)
|
||||||
|
|
||||||
|
reviewDomain := os.Getenv("REVIEW_DOMAIN")
|
||||||
|
if reviewDomain == "" {
|
||||||
|
reviewDomain = "review.example.com"
|
||||||
|
}
|
||||||
|
path := fmt.Sprintf("/%s/wip", project)
|
||||||
|
upstream := fmt.Sprintf("localhost:%d", port)
|
||||||
|
|
||||||
|
if err := s.client.AddPathRoute(reviewDomain, path, upstream); err != nil {
|
||||||
|
s.sendError(id, -32000, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
s.sendTextResult(id, fmt.Sprintf("✅ Added WIP https://%s%s/* → %s", reviewDomain, path, upstream))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Config Handlers
|
||||||
|
func (s *MCPServer) handleExportConfig(id interface{}) {
|
||||||
|
config, err := s.client.GetConfig()
|
||||||
|
if err != nil {
|
||||||
|
s.sendError(id, -32000, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
data, _ := json.MarshalIndent(config, "", " ")
|
||||||
|
s.sendTextResult(id, string(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *MCPServer) handleAdaptCaddyfile(id interface{}, args map[string]interface{}) {
|
||||||
|
caddyfile, _ := args["caddyfile"].(string)
|
||||||
|
result, err := s.client.AdaptCaddyfile(caddyfile)
|
||||||
|
if err != nil {
|
||||||
|
s.sendError(id, -32000, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
data, _ := json.MarshalIndent(result, "", " ")
|
||||||
|
s.sendTextResult(id, string(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *MCPServer) handleValidateConfig(id interface{}, args map[string]interface{}) {
|
||||||
|
configArg, _ := args["config"].(map[string]interface{})
|
||||||
|
if err := s.client.ValidateConfig(configArg); err != nil {
|
||||||
|
s.sendError(id, -32000, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
s.sendTextResult(id, "✅ Configuration is valid")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Control Handlers
|
||||||
|
func (s *MCPServer) handleCaddyStatus(id interface{}) {
|
||||||
|
sites, err := s.client.ListSites()
|
||||||
|
if err != nil {
|
||||||
|
s.sendTextResult(id, fmt.Sprintf("❌ Caddy unreachable: %v", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
s.sendTextResult(id, fmt.Sprintf("✅ Caddy running (%d sites configured)", len(sites)))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *MCPServer) handleStopCaddy(id interface{}) {
|
||||||
|
if err := s.client.Stop(); err != nil {
|
||||||
|
s.sendError(id, -32000, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
s.sendTextResult(id, "✅ Caddy stopped")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper functions
|
||||||
|
func (s *MCPServer) sendTextResult(id interface{}, text string) {
|
||||||
|
s.sendResult(id, map[string]interface{}{
|
||||||
|
"content": []interface{}{
|
||||||
|
map[string]interface{}{"type": "text", "text": text},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *MCPServer) sendResult(id interface{}, result interface{}) {
|
||||||
|
resp := map[string]interface{}{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": id,
|
||||||
|
"result": result,
|
||||||
|
}
|
||||||
|
data, _ := json.Marshal(resp)
|
||||||
|
fmt.Println(string(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *MCPServer) sendError(id interface{}, code int, message string) {
|
||||||
|
resp := map[string]interface{}{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": id,
|
||||||
|
"error": map[string]interface{}{
|
||||||
|
"code": code,
|
||||||
|
"message": message,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
data, _ := json.Marshal(resp)
|
||||||
|
fmt.Println(string(data))
|
||||||
|
}
|
||||||
+125
@@ -0,0 +1,125 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
func (c *CaddyClient) AddHeaderUp(domain, header, value string) error {
|
||||||
|
config, err := c.GetConfig()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
apps, _ := config["apps"].(map[string]interface{})
|
||||||
|
httpApps, _ := apps["http"].(map[string]interface{})
|
||||||
|
servers, _ := httpApps["servers"].(map[string]interface{})
|
||||||
|
|
||||||
|
for srvName, server := range servers {
|
||||||
|
srv, _ := server.(map[string]interface{})
|
||||||
|
routes, _ := srv["routes"].([]interface{})
|
||||||
|
|
||||||
|
for routeIdx, route := range routes {
|
||||||
|
routeMap, _ := route.(map[string]interface{})
|
||||||
|
matches, _ := routeMap["match"].([]interface{})
|
||||||
|
|
||||||
|
for _, match := range matches {
|
||||||
|
matchMap, _ := match.(map[string]interface{})
|
||||||
|
hosts, _ := matchMap["host"].([]interface{})
|
||||||
|
if len(hosts) > 0 {
|
||||||
|
d, _ := hosts[0].(string)
|
||||||
|
if d == domain {
|
||||||
|
c.addHeaderToRoute(routeMap, header, value, "request")
|
||||||
|
routes[routeIdx] = routeMap
|
||||||
|
configPath := fmt.Sprintf("apps/http/servers/%s/routes", srvName)
|
||||||
|
return c.SetConfig(configPath, routes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fmt.Errorf("site not found: %s", domain)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CaddyClient) AddHeaderDown(domain, header, value string) error {
|
||||||
|
config, err := c.GetConfig()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
apps, _ := config["apps"].(map[string]interface{})
|
||||||
|
httpApps, _ := apps["http"].(map[string]interface{})
|
||||||
|
servers, _ := httpApps["servers"].(map[string]interface{})
|
||||||
|
|
||||||
|
for srvName, server := range servers {
|
||||||
|
srv, _ := server.(map[string]interface{})
|
||||||
|
routes, _ := srv["routes"].([]interface{})
|
||||||
|
|
||||||
|
for routeIdx, route := range routes {
|
||||||
|
routeMap, _ := route.(map[string]interface{})
|
||||||
|
matches, _ := routeMap["match"].([]interface{})
|
||||||
|
|
||||||
|
for _, match := range matches {
|
||||||
|
matchMap, _ := match.(map[string]interface{})
|
||||||
|
hosts, _ := matchMap["host"].([]interface{})
|
||||||
|
if len(hosts) > 0 {
|
||||||
|
d, _ := hosts[0].(string)
|
||||||
|
if d == domain {
|
||||||
|
c.addHeaderToRoute(routeMap, header, value, "response")
|
||||||
|
routes[routeIdx] = routeMap
|
||||||
|
configPath := fmt.Sprintf("apps/http/servers/%s/routes", srvName)
|
||||||
|
return c.SetConfig(configPath, routes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fmt.Errorf("site not found: %s", domain)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CaddyClient) addHeaderToRoute(route map[string]interface{}, header, value, direction string) {
|
||||||
|
handle, _ := route["handle"].([]interface{})
|
||||||
|
for i, h := range handle {
|
||||||
|
handlerMap, ok := h.(map[string]interface{})
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if handlerMap["handler"] == "subroute" {
|
||||||
|
subRoutes, _ := handlerMap["routes"].([]interface{})
|
||||||
|
if len(subRoutes) > 0 {
|
||||||
|
mainRoute, _ := subRoutes[0].(map[string]interface{})
|
||||||
|
handlers, _ := mainRoute["handle"].([]interface{})
|
||||||
|
|
||||||
|
headersHandler := map[string]interface{}{
|
||||||
|
"handler": "headers",
|
||||||
|
}
|
||||||
|
if direction == "request" {
|
||||||
|
headersHandler["request"] = map[string]interface{}{
|
||||||
|
"set": map[string]interface{}{
|
||||||
|
header: []string{value},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
headersHandler["response"] = map[string]interface{}{
|
||||||
|
"set": map[string]interface{}{
|
||||||
|
header: []string{value},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find reverse_proxy and insert headers before it
|
||||||
|
for j, handler := range handlers {
|
||||||
|
hm, _ := handler.(map[string]interface{})
|
||||||
|
if hm["handler"] == "reverse_proxy" {
|
||||||
|
// Insert before reverse_proxy
|
||||||
|
newHandlers := append(handlers[:j], headersHandler)
|
||||||
|
newHandlers = append(newHandlers, handlers[j:]...)
|
||||||
|
mainRoute["handle"] = newHandlers
|
||||||
|
subRoutes[0] = mainRoute
|
||||||
|
handlerMap["routes"] = subRoutes
|
||||||
|
handle[i] = handlerMap
|
||||||
|
route["handle"] = handle
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+155
@@ -0,0 +1,155 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (c *CaddyClient) AddPathRoute(domain, path, upstream string) error {
|
||||||
|
config, err := c.GetConfig()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
apps, _ := config["apps"].(map[string]interface{})
|
||||||
|
httpApps, _ := apps["http"].(map[string]interface{})
|
||||||
|
servers, _ := httpApps["servers"].(map[string]interface{})
|
||||||
|
|
||||||
|
newRoute := map[string]interface{}{
|
||||||
|
"group": "group1",
|
||||||
|
"handle": []interface{}{
|
||||||
|
map[string]interface{}{
|
||||||
|
"handler": "subroute",
|
||||||
|
"routes": []interface{}{
|
||||||
|
map[string]interface{}{
|
||||||
|
"handle": []interface{}{
|
||||||
|
map[string]interface{}{
|
||||||
|
"handler": "reverse_proxy",
|
||||||
|
"upstreams": []interface{}{map[string]interface{}{"dial": upstream}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"match": []interface{}{
|
||||||
|
map[string]interface{}{
|
||||||
|
"path": []string{path + "/*"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for srvName, server := range servers {
|
||||||
|
srv, _ := server.(map[string]interface{})
|
||||||
|
routes, _ := srv["routes"].([]interface{})
|
||||||
|
|
||||||
|
for routeIdx, route := range routes {
|
||||||
|
routeMap, _ := route.(map[string]interface{})
|
||||||
|
matches, _ := routeMap["match"].([]interface{})
|
||||||
|
|
||||||
|
for _, match := range matches {
|
||||||
|
matchMap, _ := match.(map[string]interface{})
|
||||||
|
hosts, _ := matchMap["host"].([]interface{})
|
||||||
|
if len(hosts) > 0 {
|
||||||
|
d, _ := hosts[0].(string)
|
||||||
|
if d == domain {
|
||||||
|
handle, _ := routeMap["handle"].([]interface{})
|
||||||
|
for i, h := range handle {
|
||||||
|
handlerMap, ok := h.(map[string]interface{})
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if handlerMap["handler"] == "subroute" {
|
||||||
|
subRoutes, _ := handlerMap["routes"].([]interface{})
|
||||||
|
subRoutes = append(subRoutes, newRoute)
|
||||||
|
handlerMap["routes"] = subRoutes
|
||||||
|
handle[i] = handlerMap
|
||||||
|
routeMap["handle"] = handle
|
||||||
|
routes[routeIdx] = routeMap
|
||||||
|
|
||||||
|
configPath := fmt.Sprintf("apps/http/servers/%s/routes", srvName)
|
||||||
|
return c.SetConfig(configPath, routes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fmt.Errorf("site not found: %s", domain)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CaddyClient) RemovePathRoute(domain, path string) error {
|
||||||
|
config, err := c.GetConfig()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
apps, _ := config["apps"].(map[string]interface{})
|
||||||
|
httpApps, _ := apps["http"].(map[string]interface{})
|
||||||
|
servers, _ := httpApps["servers"].(map[string]interface{})
|
||||||
|
|
||||||
|
for srvName, server := range servers {
|
||||||
|
srv, _ := server.(map[string]interface{})
|
||||||
|
routes, _ := srv["routes"].([]interface{})
|
||||||
|
|
||||||
|
for routeIdx, route := range routes {
|
||||||
|
routeMap, _ := route.(map[string]interface{})
|
||||||
|
matches, _ := routeMap["match"].([]interface{})
|
||||||
|
|
||||||
|
for _, match := range matches {
|
||||||
|
matchMap, _ := match.(map[string]interface{})
|
||||||
|
hosts, _ := matchMap["host"].([]interface{})
|
||||||
|
if len(hosts) > 0 {
|
||||||
|
d, _ := hosts[0].(string)
|
||||||
|
if d == domain {
|
||||||
|
handle, _ := routeMap["handle"].([]interface{})
|
||||||
|
for i, h := range handle {
|
||||||
|
handlerMap, ok := h.(map[string]interface{})
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if handlerMap["handler"] == "subroute" {
|
||||||
|
subRoutes, _ := handlerMap["routes"].([]interface{})
|
||||||
|
var newSubRoutes []interface{}
|
||||||
|
found := false
|
||||||
|
for _, sr := range subRoutes {
|
||||||
|
srMap, ok := sr.(map[string]interface{})
|
||||||
|
if !ok {
|
||||||
|
newSubRoutes = append(newSubRoutes, sr)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
srMatches, _ := srMap["match"].([]interface{})
|
||||||
|
for _, srm := range srMatches {
|
||||||
|
srmMap, _ := srm.(map[string]interface{})
|
||||||
|
paths, _ := srmMap["path"].([]interface{})
|
||||||
|
for _, p := range paths {
|
||||||
|
ps, _ := p.(string)
|
||||||
|
if strings.HasPrefix(ps, path) {
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
newSubRoutes = append(newSubRoutes, sr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if found {
|
||||||
|
handlerMap["routes"] = newSubRoutes
|
||||||
|
handle[i] = handlerMap
|
||||||
|
routeMap["handle"] = handle
|
||||||
|
routes[routeIdx] = routeMap
|
||||||
|
|
||||||
|
configPath := fmt.Sprintf("apps/http/servers/%s/routes", srvName)
|
||||||
|
return c.SetConfig(configPath, routes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fmt.Errorf("site or path not found: %s/%s", domain, path)
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (s *MCPServer) handleGetSiteRoutes(id interface{}, args map[string]interface{}) {
|
||||||
|
domain, _ := args["domain"].(string)
|
||||||
|
config, err := s.client.GetConfig()
|
||||||
|
if err != nil {
|
||||||
|
s.sendError(id, -32000, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
apps, _ := config["apps"].(map[string]interface{})
|
||||||
|
httpApps, _ := apps["http"].(map[string]interface{})
|
||||||
|
servers, _ := httpApps["servers"].(map[string]interface{})
|
||||||
|
|
||||||
|
var output string
|
||||||
|
for _, server := range servers {
|
||||||
|
srv, _ := server.(map[string]interface{})
|
||||||
|
routes, _ := srv["routes"].([]interface{})
|
||||||
|
|
||||||
|
for _, route := range routes {
|
||||||
|
routeMap, _ := route.(map[string]interface{})
|
||||||
|
matches, _ := routeMap["match"].([]interface{})
|
||||||
|
|
||||||
|
for _, match := range matches {
|
||||||
|
matchMap, _ := match.(map[string]interface{})
|
||||||
|
hosts, _ := matchMap["host"].([]interface{})
|
||||||
|
if len(hosts) > 0 {
|
||||||
|
d, _ := hosts[0].(string)
|
||||||
|
if d == domain {
|
||||||
|
output = fmt.Sprintf("Routes for %s:\n\n", domain)
|
||||||
|
data, _ := json.MarshalIndent(routeMap, "", " ")
|
||||||
|
output += string(data)
|
||||||
|
s.sendTextResult(id, output)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
s.sendError(id, -32000, fmt.Sprintf("site not found: %s", domain))
|
||||||
|
}
|
||||||
@@ -0,0 +1,278 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"encoding/json"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MCPServer struct {
|
||||||
|
client *CaddyClient
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
caddyHost := os.Getenv("CADDY_HOST")
|
||||||
|
if caddyHost == "" {
|
||||||
|
caddyHost = "localhost:2019"
|
||||||
|
}
|
||||||
|
|
||||||
|
client := NewCaddyClient(caddyHost)
|
||||||
|
server := &MCPServer{client: client}
|
||||||
|
|
||||||
|
scanner := bufio.NewScanner(os.Stdin)
|
||||||
|
for scanner.Scan() {
|
||||||
|
line := scanner.Text()
|
||||||
|
if line == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
var req map[string]interface{}
|
||||||
|
if err := json.Unmarshal([]byte(line), &req); err != nil {
|
||||||
|
server.sendError(nil, -32700, "Parse error")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
server.handleRequest(req)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *MCPServer) handleRequest(req map[string]interface{}) {
|
||||||
|
method, _ := req["method"].(string)
|
||||||
|
id := req["id"]
|
||||||
|
|
||||||
|
switch method {
|
||||||
|
case "initialize":
|
||||||
|
s.sendResult(id, map[string]interface{}{
|
||||||
|
"protocolVersion": "2024-11-05",
|
||||||
|
"capabilities": map[string]interface{}{"tools": map[string]interface{}{}},
|
||||||
|
"serverInfo": map[string]interface{}{
|
||||||
|
"name": "caddy-mcp",
|
||||||
|
"version": "2.0.0",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
case "tools/list":
|
||||||
|
s.sendResult(id, map[string]interface{}{"tools": getAllTools()})
|
||||||
|
|
||||||
|
case "tools/call":
|
||||||
|
params, _ := req["params"].(map[string]interface{})
|
||||||
|
toolName, _ := params["name"].(string)
|
||||||
|
args, _ := params["arguments"].(map[string]interface{})
|
||||||
|
s.handleToolCall(id, toolName, args)
|
||||||
|
|
||||||
|
default:
|
||||||
|
s.sendError(id, -32601, "Method not found")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getAllTools() []interface{} {
|
||||||
|
return []interface{}{
|
||||||
|
// Site management
|
||||||
|
map[string]interface{}{
|
||||||
|
"name": "list_sites",
|
||||||
|
"description": "List all configured Caddy sites with their upstreams",
|
||||||
|
"inputSchema": map[string]interface{}{"type": "object", "properties": map[string]interface{}{}},
|
||||||
|
},
|
||||||
|
map[string]interface{}{
|
||||||
|
"name": "get_site",
|
||||||
|
"description": "Get detailed configuration for a specific site",
|
||||||
|
"inputSchema": map[string]interface{}{
|
||||||
|
"type": "object",
|
||||||
|
"properties": map[string]interface{}{"domain": map[string]interface{}{"type": "string", "description": "Domain name"}},
|
||||||
|
"required": []string{"domain"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
map[string]interface{}{
|
||||||
|
"name": "get_site_routes",
|
||||||
|
"description": "Get all routes (including path-based) for a site",
|
||||||
|
"inputSchema": map[string]interface{}{
|
||||||
|
"type": "object",
|
||||||
|
"properties": map[string]interface{}{"domain": map[string]interface{}{"type": "string", "description": "Domain name"}},
|
||||||
|
"required": []string{"domain"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
map[string]interface{}{
|
||||||
|
"name": "add_site",
|
||||||
|
"description": "Add a new site with reverse proxy",
|
||||||
|
"inputSchema": map[string]interface{}{
|
||||||
|
"type": "object",
|
||||||
|
"properties": map[string]interface{}{
|
||||||
|
"domain": map[string]interface{}{"type": "string", "description": "Domain name"},
|
||||||
|
"upstream": map[string]interface{}{"type": "string", "description": "Upstream address (e.g., localhost:3000)"},
|
||||||
|
},
|
||||||
|
"required": []string{"domain", "upstream"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
map[string]interface{}{
|
||||||
|
"name": "remove_site",
|
||||||
|
"description": "Remove a site configuration",
|
||||||
|
"inputSchema": map[string]interface{}{
|
||||||
|
"type": "object",
|
||||||
|
"properties": map[string]interface{}{"domain": map[string]interface{}{"type": "string", "description": "Domain to remove"}},
|
||||||
|
"required": []string{"domain"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
map[string]interface{}{
|
||||||
|
"name": "update_upstream",
|
||||||
|
"description": "Update the upstream address for a site",
|
||||||
|
"inputSchema": map[string]interface{}{
|
||||||
|
"type": "object",
|
||||||
|
"properties": map[string]interface{}{
|
||||||
|
"domain": map[string]interface{}{"type": "string", "description": "Domain name"},
|
||||||
|
"upstream": map[string]interface{}{"type": "string", "description": "New upstream address"},
|
||||||
|
},
|
||||||
|
"required": []string{"domain", "upstream"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// Path-based routing
|
||||||
|
map[string]interface{}{
|
||||||
|
"name": "add_path_route",
|
||||||
|
"description": "Add a path-based route to a site (e.g., /api/* → localhost:3000)",
|
||||||
|
"inputSchema": map[string]interface{}{
|
||||||
|
"type": "object",
|
||||||
|
"properties": map[string]interface{}{
|
||||||
|
"domain": map[string]interface{}{"type": "string", "description": "Domain name"},
|
||||||
|
"path": map[string]interface{}{"type": "string", "description": "Path pattern (e.g., /api)"},
|
||||||
|
"upstream": map[string]interface{}{"type": "string", "description": "Upstream address"},
|
||||||
|
},
|
||||||
|
"required": []string{"domain", "path", "upstream"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
map[string]interface{}{
|
||||||
|
"name": "remove_path_route",
|
||||||
|
"description": "Remove a path-based route from a site",
|
||||||
|
"inputSchema": map[string]interface{}{
|
||||||
|
"type": "object",
|
||||||
|
"properties": map[string]interface{}{
|
||||||
|
"domain": map[string]interface{}{"type": "string", "description": "Domain name"},
|
||||||
|
"path": map[string]interface{}{"type": "string", "description": "Path pattern to remove"},
|
||||||
|
},
|
||||||
|
"required": []string{"domain", "path"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// Authentication
|
||||||
|
map[string]interface{}{
|
||||||
|
"name": "add_basic_auth",
|
||||||
|
"description": "Add basic authentication to a site",
|
||||||
|
"inputSchema": map[string]interface{}{
|
||||||
|
"type": "object",
|
||||||
|
"properties": map[string]interface{}{
|
||||||
|
"domain": map[string]interface{}{"type": "string", "description": "Domain name"},
|
||||||
|
"username": map[string]interface{}{"type": "string", "description": "Username"},
|
||||||
|
"password": map[string]interface{}{"type": "string", "description": "Password (will be hashed)"},
|
||||||
|
},
|
||||||
|
"required": []string{"domain", "username", "password"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
map[string]interface{}{
|
||||||
|
"name": "remove_basic_auth",
|
||||||
|
"description": "Remove basic authentication from a site",
|
||||||
|
"inputSchema": map[string]interface{}{
|
||||||
|
"type": "object",
|
||||||
|
"properties": map[string]interface{}{"domain": map[string]interface{}{"type": "string", "description": "Domain name"}},
|
||||||
|
"required": []string{"domain"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// Headers
|
||||||
|
map[string]interface{}{
|
||||||
|
"name": "add_request_header",
|
||||||
|
"description": "Add a request header sent to upstream",
|
||||||
|
"inputSchema": map[string]interface{}{
|
||||||
|
"type": "object",
|
||||||
|
"properties": map[string]interface{}{
|
||||||
|
"domain": map[string]interface{}{"type": "string", "description": "Domain name"},
|
||||||
|
"header": map[string]interface{}{"type": "string", "description": "Header name"},
|
||||||
|
"value": map[string]interface{}{"type": "string", "description": "Header value"},
|
||||||
|
},
|
||||||
|
"required": []string{"domain", "header", "value"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
map[string]interface{}{
|
||||||
|
"name": "add_response_header",
|
||||||
|
"description": "Add a response header sent to client",
|
||||||
|
"inputSchema": map[string]interface{}{
|
||||||
|
"type": "object",
|
||||||
|
"properties": map[string]interface{}{
|
||||||
|
"domain": map[string]interface{}{"type": "string", "description": "Domain name"},
|
||||||
|
"header": map[string]interface{}{"type": "string", "description": "Header name"},
|
||||||
|
"value": map[string]interface{}{"type": "string", "description": "Header value"},
|
||||||
|
},
|
||||||
|
"required": []string{"domain", "header", "value"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// File server
|
||||||
|
map[string]interface{}{
|
||||||
|
"name": "add_file_server",
|
||||||
|
"description": "Add a static file server for a domain",
|
||||||
|
"inputSchema": map[string]interface{}{
|
||||||
|
"type": "object",
|
||||||
|
"properties": map[string]interface{}{
|
||||||
|
"domain": map[string]interface{}{"type": "string", "description": "Domain name"},
|
||||||
|
"root": map[string]interface{}{"type": "string", "description": "Root directory path"},
|
||||||
|
"browse": map[string]interface{}{"type": "boolean", "description": "Enable directory browsing"},
|
||||||
|
},
|
||||||
|
"required": []string{"domain", "root"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
map[string]interface{}{
|
||||||
|
"name": "update_file_root",
|
||||||
|
"description": "Update the root directory for a file server",
|
||||||
|
"inputSchema": map[string]interface{}{
|
||||||
|
"type": "object",
|
||||||
|
"properties": map[string]interface{}{
|
||||||
|
"domain": map[string]interface{}{"type": "string", "description": "Domain name"},
|
||||||
|
"root": map[string]interface{}{"type": "string", "description": "New root directory"},
|
||||||
|
},
|
||||||
|
"required": []string{"domain", "root"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// Review environment helpers
|
||||||
|
map[string]interface{}{
|
||||||
|
"name": "add_wip_environment",
|
||||||
|
"description": "Add a WIP review environment (e.g., /project/wip/* → port)",
|
||||||
|
"inputSchema": map[string]interface{}{
|
||||||
|
"type": "object",
|
||||||
|
"properties": map[string]interface{}{
|
||||||
|
"project": map[string]interface{}{"type": "string", "description": "Project name (e.g., my-project, frontend)"},
|
||||||
|
"port": map[string]interface{}{"type": "number", "description": "Port number"},
|
||||||
|
},
|
||||||
|
"required": []string{"project", "port"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// Configuration
|
||||||
|
map[string]interface{}{
|
||||||
|
"name": "export_config",
|
||||||
|
"description": "Export full Caddy configuration as JSON",
|
||||||
|
"inputSchema": map[string]interface{}{"type": "object", "properties": map[string]interface{}{}},
|
||||||
|
},
|
||||||
|
map[string]interface{}{
|
||||||
|
"name": "adapt_caddyfile",
|
||||||
|
"description": "Convert Caddyfile format to JSON",
|
||||||
|
"inputSchema": map[string]interface{}{
|
||||||
|
"type": "object",
|
||||||
|
"properties": map[string]interface{}{"caddyfile": map[string]interface{}{"type": "string", "description": "Caddyfile content"}},
|
||||||
|
"required": []string{"caddyfile"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// Server control
|
||||||
|
map[string]interface{}{
|
||||||
|
"name": "caddy_status",
|
||||||
|
"description": "Check Caddy server health and status",
|
||||||
|
"inputSchema": map[string]interface{}{"type": "object", "properties": map[string]interface{}{}},
|
||||||
|
},
|
||||||
|
map[string]interface{}{
|
||||||
|
"name": "validate_config",
|
||||||
|
"description": "Validate a configuration without applying it",
|
||||||
|
"inputSchema": map[string]interface{}{
|
||||||
|
"type": "object",
|
||||||
|
"properties": map[string]interface{}{"config": map[string]interface{}{"type": "object", "description": "Configuration to validate"}},
|
||||||
|
"required": []string{"config"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
map[string]interface{}{
|
||||||
|
"name": "stop_caddy",
|
||||||
|
"description": "Stop the Caddy server gracefully",
|
||||||
|
"inputSchema": map[string]interface{}{"type": "object", "properties": map[string]interface{}{}},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
+163
@@ -0,0 +1,163 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
func (c *CaddyClient) AddSite(domain, upstream string) error {
|
||||||
|
route := map[string]interface{}{
|
||||||
|
"handle": []interface{}{
|
||||||
|
map[string]interface{}{
|
||||||
|
"handler": "subroute",
|
||||||
|
"routes": []interface{}{
|
||||||
|
map[string]interface{}{
|
||||||
|
"handle": []interface{}{
|
||||||
|
map[string]interface{}{
|
||||||
|
"handler": "reverse_proxy",
|
||||||
|
"upstreams": []interface{}{map[string]interface{}{"dial": upstream}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"match": []interface{}{map[string]interface{}{"host": []string{domain}}},
|
||||||
|
"terminal": true,
|
||||||
|
}
|
||||||
|
|
||||||
|
config, _ := c.GetConfig()
|
||||||
|
apps, _ := config["apps"].(map[string]interface{})
|
||||||
|
httpApps, _ := apps["http"].(map[string]interface{})
|
||||||
|
servers, _ := httpApps["servers"].(map[string]interface{})
|
||||||
|
|
||||||
|
for srvName := range servers {
|
||||||
|
routesPath := fmt.Sprintf("apps/http/servers/%s/routes", srvName)
|
||||||
|
routes, _ := c.GetConfigAtPath(routesPath)
|
||||||
|
if routes != nil {
|
||||||
|
routesList, _ := routes.([]interface{})
|
||||||
|
routesList = append(routesList, route)
|
||||||
|
return c.SetConfig(routesPath, routesList)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.SetConfig("apps/http/servers/srv0/routes", []interface{}{route})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CaddyClient) RemoveSite(domain string) error {
|
||||||
|
config, err := c.GetConfig()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
apps, _ := config["apps"].(map[string]interface{})
|
||||||
|
httpApps, _ := apps["http"].(map[string]interface{})
|
||||||
|
servers, _ := httpApps["servers"].(map[string]interface{})
|
||||||
|
|
||||||
|
for srvName, server := range servers {
|
||||||
|
srv, _ := server.(map[string]interface{})
|
||||||
|
routes, _ := srv["routes"].([]interface{})
|
||||||
|
|
||||||
|
var newRoutes []interface{}
|
||||||
|
found := false
|
||||||
|
for _, route := range routes {
|
||||||
|
routeMap, _ := route.(map[string]interface{})
|
||||||
|
matches, _ := routeMap["match"].([]interface{})
|
||||||
|
|
||||||
|
for _, match := range matches {
|
||||||
|
matchMap, _ := match.(map[string]interface{})
|
||||||
|
hosts, _ := matchMap["host"].([]interface{})
|
||||||
|
if len(hosts) > 0 {
|
||||||
|
d, _ := hosts[0].(string)
|
||||||
|
if d == domain {
|
||||||
|
found = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
newRoutes = append(newRoutes, route)
|
||||||
|
}
|
||||||
|
|
||||||
|
if found {
|
||||||
|
path := fmt.Sprintf("apps/http/servers/%s/routes", srvName)
|
||||||
|
return c.SetConfig(path, newRoutes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fmt.Errorf("site not found: %s", domain)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CaddyClient) UpdateUpstream(domain, upstream string) error {
|
||||||
|
config, err := c.GetConfig()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
apps, _ := config["apps"].(map[string]interface{})
|
||||||
|
httpApps, _ := apps["http"].(map[string]interface{})
|
||||||
|
servers, _ := httpApps["servers"].(map[string]interface{})
|
||||||
|
|
||||||
|
for srvName, server := range servers {
|
||||||
|
srv, _ := server.(map[string]interface{})
|
||||||
|
routes, _ := srv["routes"].([]interface{})
|
||||||
|
|
||||||
|
for i, route := range routes {
|
||||||
|
routeMap, _ := route.(map[string]interface{})
|
||||||
|
matches, _ := routeMap["match"].([]interface{})
|
||||||
|
|
||||||
|
for _, match := range matches {
|
||||||
|
matchMap, _ := match.(map[string]interface{})
|
||||||
|
hosts, _ := matchMap["host"].([]interface{})
|
||||||
|
if len(hosts) > 0 {
|
||||||
|
d, _ := hosts[0].(string)
|
||||||
|
if d == domain {
|
||||||
|
newRoute := c.updateUpstreamInRoute(routeMap, upstream)
|
||||||
|
routes[i] = newRoute
|
||||||
|
path := fmt.Sprintf("apps/http/servers/%s/routes", srvName)
|
||||||
|
return c.SetConfig(path, routes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fmt.Errorf("site not found: %s", domain)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CaddyClient) updateUpstreamInRoute(route map[string]interface{}, upstream string) map[string]interface{} {
|
||||||
|
handle, _ := route["handle"].([]interface{})
|
||||||
|
for i, h := range handle {
|
||||||
|
handlerMap, ok := h.(map[string]interface{})
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if handlerMap["handler"] == "reverse_proxy" {
|
||||||
|
handlerMap["upstreams"] = []interface{}{map[string]interface{}{"dial": upstream}}
|
||||||
|
handle[i] = handlerMap
|
||||||
|
route["handle"] = handle
|
||||||
|
return route
|
||||||
|
}
|
||||||
|
if handlerMap["handler"] == "subroute" {
|
||||||
|
subRoutes, _ := handlerMap["routes"].([]interface{})
|
||||||
|
for j, sr := range subRoutes {
|
||||||
|
srMap, ok := sr.(map[string]interface{})
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
srHandlers, _ := srMap["handle"].([]interface{})
|
||||||
|
for k, srH := range srHandlers {
|
||||||
|
srHMap, ok := srH.(map[string]interface{})
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if srHMap["handler"] == "reverse_proxy" {
|
||||||
|
srHMap["upstreams"] = []interface{}{map[string]interface{}{"dial": upstream}}
|
||||||
|
srHandlers[k] = srHMap
|
||||||
|
srMap["handle"] = srHandlers
|
||||||
|
subRoutes[j] = srMap
|
||||||
|
handlerMap["routes"] = subRoutes
|
||||||
|
handle[i] = handlerMap
|
||||||
|
route["handle"] = handle
|
||||||
|
return route
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return route
|
||||||
|
}
|
||||||
+179
@@ -0,0 +1,179 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (c *CaddyClient) ListSites() ([]map[string]interface{}, error) {
|
||||||
|
config, err := c.GetConfig()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return c.extractSitesFromConfig(config)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CaddyClient) extractSitesFromConfig(config map[string]interface{}) ([]map[string]interface{}, error) {
|
||||||
|
apps, _ := config["apps"].(map[string]interface{})
|
||||||
|
httpApps, _ := apps["http"].(map[string]interface{})
|
||||||
|
servers, _ := httpApps["servers"].(map[string]interface{})
|
||||||
|
|
||||||
|
var sites []map[string]interface{}
|
||||||
|
for serverName, server := range servers {
|
||||||
|
srv, _ := server.(map[string]interface{})
|
||||||
|
routes, _ := srv["routes"].([]interface{})
|
||||||
|
|
||||||
|
for _, route := range routes {
|
||||||
|
routeMap, _ := route.(map[string]interface{})
|
||||||
|
matches, _ := routeMap["match"].([]interface{})
|
||||||
|
|
||||||
|
for _, match := range matches {
|
||||||
|
matchMap, _ := match.(map[string]interface{})
|
||||||
|
hosts, _ := matchMap["host"].([]interface{})
|
||||||
|
if len(hosts) > 0 {
|
||||||
|
domain, _ := hosts[0].(string)
|
||||||
|
site := map[string]interface{}{
|
||||||
|
"domain": domain,
|
||||||
|
"server": serverName,
|
||||||
|
"route": routeMap,
|
||||||
|
}
|
||||||
|
sites = append(sites, site)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return sites, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CaddyClient) GetSite(domain string) (map[string]interface{}, error) {
|
||||||
|
config, err := c.GetConfig()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
apps, _ := config["apps"].(map[string]interface{})
|
||||||
|
httpApps, _ := apps["http"].(map[string]interface{})
|
||||||
|
servers, _ := httpApps["servers"].(map[string]interface{})
|
||||||
|
|
||||||
|
for serverName, server := range servers {
|
||||||
|
srv, _ := server.(map[string]interface{})
|
||||||
|
routes, _ := srv["routes"].([]interface{})
|
||||||
|
|
||||||
|
for _, route := range routes {
|
||||||
|
routeMap, _ := route.(map[string]interface{})
|
||||||
|
matches, _ := routeMap["match"].([]interface{})
|
||||||
|
|
||||||
|
for _, match := range matches {
|
||||||
|
matchMap, _ := match.(map[string]interface{})
|
||||||
|
hosts, _ := matchMap["host"].([]interface{})
|
||||||
|
if len(hosts) > 0 {
|
||||||
|
d, _ := hosts[0].(string)
|
||||||
|
if d == domain {
|
||||||
|
site := map[string]interface{}{
|
||||||
|
"domain": domain,
|
||||||
|
"server": serverName,
|
||||||
|
"route": routeMap,
|
||||||
|
"handlers": c.extractHandlers(routeMap),
|
||||||
|
"upstreams": c.extractUpstreams(routeMap),
|
||||||
|
"has_auth": c.hasBasicAuth(routeMap),
|
||||||
|
}
|
||||||
|
return site, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("site not found: %s", domain)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CaddyClient) extractHandlers(route map[string]interface{}) []map[string]interface{} {
|
||||||
|
var handlers []map[string]interface{}
|
||||||
|
handle, _ := route["handle"].([]interface{})
|
||||||
|
for _, h := range handle {
|
||||||
|
handlerMap, ok := h.(map[string]interface{})
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
handlers = append(handlers, handlerMap)
|
||||||
|
}
|
||||||
|
return handlers
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CaddyClient) extractUpstreams(route map[string]interface{}) []string {
|
||||||
|
var upstreams []string
|
||||||
|
handle, _ := route["handle"].([]interface{})
|
||||||
|
for _, h := range handle {
|
||||||
|
handlerMap, ok := h.(map[string]interface{})
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if handlerMap["handler"] == "reverse_proxy" {
|
||||||
|
ups, _ := handlerMap["upstreams"].([]interface{})
|
||||||
|
for _, u := range ups {
|
||||||
|
upMap, ok := u.(map[string]interface{})
|
||||||
|
if ok {
|
||||||
|
dial, _ := upMap["dial"].(string)
|
||||||
|
if dial != "" {
|
||||||
|
upstreams = append(upstreams, dial)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if handlerMap["handler"] == "subroute" {
|
||||||
|
subRoutes, _ := handlerMap["routes"].([]interface{})
|
||||||
|
for _, sr := range subRoutes {
|
||||||
|
srMap, ok := sr.(map[string]interface{})
|
||||||
|
if ok {
|
||||||
|
srHandlers, _ := srMap["handle"].([]interface{})
|
||||||
|
for _, srH := range srHandlers {
|
||||||
|
srHMap, ok := srH.(map[string]interface{})
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if srHMap["handler"] == "reverse_proxy" {
|
||||||
|
ups, _ := srHMap["upstreams"].([]interface{})
|
||||||
|
for _, u := range ups {
|
||||||
|
upMap, ok := u.(map[string]interface{})
|
||||||
|
if ok {
|
||||||
|
dial, _ := upMap["dial"].(string)
|
||||||
|
if dial != "" {
|
||||||
|
upstreams = append(upstreams, dial)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return upstreams
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CaddyClient) hasBasicAuth(route map[string]interface{}) bool {
|
||||||
|
handlers := c.extractHandlers(route)
|
||||||
|
for _, h := range handlers {
|
||||||
|
if h["handler"] == "authentication" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if h["handler"] == "subroute" {
|
||||||
|
subRoutes, _ := h["routes"].([]interface{})
|
||||||
|
for _, sr := range subRoutes {
|
||||||
|
srMap, ok := sr.(map[string]interface{})
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
srHandlers, _ := srMap["handle"].([]interface{})
|
||||||
|
for _, srH := range srHandlers {
|
||||||
|
srHMap, ok := srH.(map[string]interface{})
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if srHMap["handler"] == "authentication" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Test script for caddy-mcp
|
||||||
|
|
||||||
|
echo 'Testing list_sites...'
|
||||||
|
echo '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"list_sites","arguments":{}}}' | ./caddy-mcp
|
||||||
|
|
||||||
|
echo ''
|
||||||
|
echo 'Testing caddy_status...'
|
||||||
|
echo '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"caddy_status","arguments":{}}}' | ./caddy-mcp
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
func (s *MCPServer) handleToolCall(id interface{}, toolName string, args map[string]interface{}) {
|
||||||
|
switch toolName {
|
||||||
|
case "list_sites":
|
||||||
|
s.handleListSites(id)
|
||||||
|
case "get_site":
|
||||||
|
s.handleGetSite(id, args)
|
||||||
|
case "get_site_routes":
|
||||||
|
s.handleGetSiteRoutes(id, args)
|
||||||
|
case "add_site":
|
||||||
|
s.handleAddSite(id, args)
|
||||||
|
case "remove_site":
|
||||||
|
s.handleRemoveSite(id, args)
|
||||||
|
case "update_upstream":
|
||||||
|
s.handleUpdateUpstream(id, args)
|
||||||
|
case "add_path_route":
|
||||||
|
s.handleAddPathRoute(id, args)
|
||||||
|
case "remove_path_route":
|
||||||
|
s.handleRemovePathRoute(id, args)
|
||||||
|
case "add_basic_auth":
|
||||||
|
s.handleAddBasicAuth(id, args)
|
||||||
|
case "remove_basic_auth":
|
||||||
|
s.handleRemoveBasicAuth(id, args)
|
||||||
|
case "add_request_header":
|
||||||
|
s.handleAddRequestHeader(id, args)
|
||||||
|
case "add_response_header":
|
||||||
|
s.handleAddResponseHeader(id, args)
|
||||||
|
case "add_file_server":
|
||||||
|
s.handleAddFileServer(id, args)
|
||||||
|
case "update_file_root":
|
||||||
|
s.handleUpdateFileRoot(id, args)
|
||||||
|
case "add_wip_environment":
|
||||||
|
s.handleAddWipEnvironment(id, args)
|
||||||
|
case "export_config":
|
||||||
|
s.handleExportConfig(id)
|
||||||
|
case "adapt_caddyfile":
|
||||||
|
s.handleAdaptCaddyfile(id, args)
|
||||||
|
case "validate_config":
|
||||||
|
s.handleValidateConfig(id, args)
|
||||||
|
case "caddy_status":
|
||||||
|
s.handleCaddyStatus(id)
|
||||||
|
case "stop_caddy":
|
||||||
|
s.handleStopCaddy(id)
|
||||||
|
default:
|
||||||
|
s.sendError(id, -32601, fmt.Sprintf("Unknown tool: %s", toolName))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/base64"
|
||||||
|
)
|
||||||
|
|
||||||
|
func generateSalt() (string, error) {
|
||||||
|
salt := make([]byte, 16)
|
||||||
|
if _, err := rand.Read(salt); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return base64.RawStdEncoding.EncodeToString(salt), nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"regexp"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func validateDomain(domain string) error {
|
||||||
|
if domain == "" {
|
||||||
|
return fmt.Errorf("domain cannot be empty")
|
||||||
|
}
|
||||||
|
domainRegex := regexp.MustCompile(`^(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)*[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$`)
|
||||||
|
if !domainRegex.MatchString(domain) {
|
||||||
|
return fmt.Errorf("invalid domain format: %s", domain)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func validatePath(path string) error {
|
||||||
|
if path == "" {
|
||||||
|
return fmt.Errorf("path cannot be empty")
|
||||||
|
}
|
||||||
|
if strings.Contains(path, "..") {
|
||||||
|
return fmt.Errorf("path cannot contain parent directory references (..)")
|
||||||
|
}
|
||||||
|
if !strings.HasPrefix(path, "/") {
|
||||||
|
return fmt.Errorf("path must start with /")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func validatePort(port string) error {
|
||||||
|
if port == "" {
|
||||||
|
return fmt.Errorf("port cannot be empty")
|
||||||
|
}
|
||||||
|
p, err := strconv.Atoi(port)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("invalid port number: %s", port)
|
||||||
|
}
|
||||||
|
if p < 1 || p > 65535 {
|
||||||
|
return fmt.Errorf("port must be between 1 and 65535, got: %d", p)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateHeaderName(name string) error {
|
||||||
|
if name == "" {
|
||||||
|
return fmt.Errorf("header name cannot be empty")
|
||||||
|
}
|
||||||
|
headerRegex := regexp.MustCompile("^[a-zA-Z0-9!#$%&'*+\\-\\.^_`\\|~]+$")
|
||||||
|
if !headerRegex.MatchString(name) {
|
||||||
|
return fmt.Errorf("invalid header name format: %s (must be valid HTTP token characters only)", name)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user