Skip to content

Commit 14f42c6

Browse files
authored
fixes for MCP build/release (#515)
This PR enables to create tag with the binaries in semi-manual mode. To create a new release, go to Github Actions, choose "Release MCP Server" and choose tag. Mac binaries may need to use the MCP: ``` xattr -d com.apple.quarantine ./dabgent_mcp-macos-arm64 ```
1 parent 2a589fe commit 14f42c6

File tree

4 files changed

+220
-5
lines changed

4 files changed

+220
-5
lines changed

.github/workflows/release.yml

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,25 @@
1-
name: Release
1+
name: Release MCP server
22

33
on:
44
workflow_dispatch:
55
inputs:
66
version:
7-
description: 'Version tag (e.g., v0.0.1)'
7+
description: "Version tag (e.g., v0.0.1)"
88
required: true
99
type: string
1010
commit_sha:
11-
description: 'Commit SHA to release'
11+
description: "Commit SHA to release"
1212
required: true
1313
type: string
1414
release_notes:
15-
description: 'Release notes'
15+
description: "Release notes"
1616
required: false
1717
type: string
18-
default: ''
18+
default: ""
19+
20+
permissions:
21+
contents: write
22+
actions: read
1923

2024
jobs:
2125
validate-and-tag:

.github/workflows/rust.yml

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,3 +134,36 @@ jobs:
134134
name: dabgent-mcp-${{ matrix.platform }}
135135
path: dabgent/target/release/dabgent_mcp
136136
retention-days: 30
137+
138+
smoke-test-mcp:
139+
name: Smoke Test MCP Binary (${{ matrix.platform }})
140+
runs-on: ${{ matrix.runner }}
141+
timeout-minutes: 5
142+
needs: build-dabgent-mcp
143+
strategy:
144+
matrix:
145+
include:
146+
- platform: linux-x86_64
147+
runner: ubuntu-latest
148+
- platform: macos-arm64
149+
runner: macos-latest
150+
steps:
151+
- name: Checkout repository
152+
uses: actions/checkout@v3
153+
154+
- name: Download binary artifact
155+
uses: actions/download-artifact@v4
156+
with:
157+
name: dabgent-mcp-${{ matrix.platform }}
158+
path: ./binary
159+
160+
- name: Make binary executable
161+
run: chmod +x ./binary/dabgent_mcp
162+
163+
- name: Setup Python
164+
uses: actions/setup-python@v4
165+
with:
166+
python-version: '3.11'
167+
168+
- name: Run smoke test
169+
run: python3 scripts/smoke_test_mcp.py ./binary/dabgent_mcp

dabgent/dabgent_mcp/src/main.rs

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,54 @@
11
use dabgent_mcp::providers::{
22
CombinedProvider, DatabricksProvider, DeploymentProvider, IOProvider, GoogleSheetsProvider,
33
};
4+
use dabgent_sandbox::dagger::{ConnectOpts, Logger};
5+
use dabgent_sandbox::{DaggerSandbox, Sandbox};
46
use eyre::Result;
57
use rmcp::ServiceExt;
68
use rmcp::transport::stdio;
79
use tracing_subscriber;
810

11+
/// check if docker is available by running 'docker ps'
12+
async fn check_docker_available() -> Result<()> {
13+
let output = tokio::process::Command::new("docker")
14+
.arg("ps")
15+
.output()
16+
.await;
17+
18+
match output {
19+
Ok(output) if output.status.success() => Ok(()),
20+
Ok(_) => Err(eyre::eyre!(
21+
"docker command found but not responding (is the daemon running?)"
22+
)),
23+
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
24+
Err(eyre::eyre!("docker command not found"))
25+
}
26+
Err(e) => Err(eyre::eyre!("failed to check docker: {}", e)),
27+
}
28+
}
29+
30+
/// warmup sandbox by pre-pulling node image and creating a test container
31+
async fn warmup_sandbox() -> Result<()> {
32+
let opts = ConnectOpts::default()
33+
.with_logger(Logger::Silent)
34+
.with_execute_timeout(Some(600));
35+
36+
opts.connect(|client| async move {
37+
let container = client
38+
.container()
39+
.from("node:20-alpine3.22")
40+
.with_exec(vec!["mkdir", "-p", "/app"]);
41+
let sandbox = DaggerSandbox::from_container(container, client);
42+
// force evaluation to ensure image is pulled
43+
let _ = sandbox.list_directory("/app").await?;
44+
Ok(())
45+
})
46+
.await
47+
.map_err(|e| eyre::eyre!("dagger connect failed: {}", e))?;
48+
49+
Ok(())
50+
}
51+
952
#[tokio::main]
1053
async fn main() -> Result<()> {
1154
// configure tracing to write to stderr only if RUST_LOG is set
@@ -22,6 +65,21 @@ async fn main() -> Result<()> {
2265
.init();
2366
}
2467

68+
// check if docker is available before initializing providers
69+
let docker_available = check_docker_available().await.is_ok();
70+
if !docker_available {
71+
eprintln!("⚠️ Warning: docker not available - you may have issues with sandbox operations\n");
72+
}
73+
74+
// spawn non-blocking warmup task if docker is available
75+
if docker_available {
76+
tokio::spawn(async {
77+
if let Err(e) = warmup_sandbox().await {
78+
eprintln!("⚠️ Sandbox warmup failed: {}", e);
79+
}
80+
});
81+
}
82+
2583
// initialize all available providers
2684
let databricks = DatabricksProvider::new().ok();
2785
let deployment = DeploymentProvider::new().ok();

scripts/smoke_test_mcp.py

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
#!/usr/bin/env python3
2+
"""
3+
Smoke test for dabgent_mcp binary.
4+
Verifies the MCP server starts, responds to protocol messages, and lists tools.
5+
"""
6+
7+
import json
8+
import subprocess
9+
import sys
10+
from pathlib import Path
11+
12+
13+
def send_request(process: subprocess.Popen, request: dict) -> dict:
14+
"""send JSON-RPC request and read response"""
15+
request_line = json.dumps(request) + "\n"
16+
process.stdin.write(request_line.encode())
17+
process.stdin.flush()
18+
19+
response_line = process.stdout.readline().decode().strip()
20+
if not response_line:
21+
raise RuntimeError("no response from MCP server")
22+
23+
return json.loads(response_line)
24+
25+
26+
def main() -> None:
27+
if len(sys.argv) != 2:
28+
print(f"Usage: {sys.argv[0]} <path-to-dabgent_mcp-binary>")
29+
sys.exit(1)
30+
31+
binary_path = Path(sys.argv[1])
32+
if not binary_path.exists():
33+
print(f"Error: Binary not found at {binary_path}")
34+
sys.exit(1)
35+
36+
print(f"Starting MCP server: {binary_path}")
37+
38+
# start the MCP server
39+
process = subprocess.Popen(
40+
[str(binary_path)],
41+
stdin=subprocess.PIPE,
42+
stdout=subprocess.PIPE,
43+
stderr=subprocess.PIPE,
44+
)
45+
46+
try:
47+
# 1. initialize request
48+
print("\n1. Sending initialize request...")
49+
init_response = send_request(
50+
process,
51+
{
52+
"jsonrpc": "2.0",
53+
"id": 1,
54+
"method": "initialize",
55+
"params": {
56+
"protocolVersion": "2024-11-05",
57+
"capabilities": {},
58+
"clientInfo": {"name": "smoke-test", "version": "1.0.0"},
59+
},
60+
},
61+
)
62+
63+
if "result" not in init_response:
64+
print(f"❌ Initialize failed: {init_response}")
65+
sys.exit(1)
66+
67+
server_info = init_response["result"]
68+
print(f"✓ Server initialized: {server_info.get('serverInfo', {})}")
69+
70+
# send initialized notification (no response expected)
71+
print("\n2. Sending initialized notification...")
72+
notification = json.dumps({"jsonrpc": "2.0", "method": "notifications/initialized"}) + "\n"
73+
process.stdin.write(notification.encode())
74+
process.stdin.flush()
75+
print("✓ Initialized notification sent")
76+
77+
# 3. list tools request
78+
print("\n3. Sending tools/list request...")
79+
tools_response = send_request(
80+
process,
81+
{"jsonrpc": "2.0", "id": 2, "method": "tools/list", "params": {}},
82+
)
83+
84+
if "result" not in tools_response:
85+
print(f"❌ tools/list failed: {tools_response}")
86+
sys.exit(1)
87+
88+
tools = tools_response["result"].get("tools", [])
89+
tool_names = [tool["name"] for tool in tools]
90+
91+
print(f"\n✓ Found {len(tools)} tools:")
92+
for name in sorted(tool_names):
93+
print(f" - {name}")
94+
95+
# 4. verify expected tools (IOProvider is always available)
96+
expected_tools = ["scaffold_data_app", "validate_data_app"]
97+
missing_tools = [tool for tool in expected_tools if tool not in tool_names]
98+
99+
if missing_tools:
100+
print(f"\n❌ Missing expected tools: {missing_tools}")
101+
sys.exit(1)
102+
103+
print(f"\n✓ All expected tools present: {expected_tools}")
104+
105+
print("\n✅ Smoke test passed!")
106+
107+
except Exception as e:
108+
print(f"\n❌ Smoke test failed: {e}")
109+
stderr = process.stderr.read().decode()
110+
if stderr:
111+
print(f"\nServer stderr:\n{stderr}")
112+
sys.exit(1)
113+
114+
finally:
115+
process.terminate()
116+
process.wait(timeout=5)
117+
118+
119+
if __name__ == "__main__":
120+
main()

0 commit comments

Comments
 (0)