agenticros 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/config.d.ts.map +1 -1
- package/dist/commands/config.js +37 -1
- package/dist/commands/config.js.map +1 -1
- package/dist/commands/down.d.ts +6 -4
- package/dist/commands/down.d.ts.map +1 -1
- package/dist/commands/down.js +16 -6
- package/dist/commands/down.js.map +1 -1
- package/dist/commands/logs.js +2 -2
- package/dist/commands/logs.js.map +1 -1
- package/dist/commands/status.d.ts.map +1 -1
- package/dist/commands/status.js +10 -6
- package/dist/commands/status.js.map +1 -1
- package/dist/commands/up.d.ts +1 -0
- package/dist/commands/up.d.ts.map +1 -1
- package/dist/commands/up.js +33 -1
- package/dist/commands/up.js.map +1 -1
- package/dist/index.js +25 -4
- package/dist/index.js.map +1 -1
- package/dist/runners/sim.d.ts +2 -0
- package/dist/runners/sim.d.ts.map +1 -1
- package/dist/runners/sim.js +2 -0
- package/dist/runners/sim.js.map +1 -1
- package/package.json +1 -1
- package/runtime/BUNDLE.json +1 -1
- package/runtime/packages/core/src/transport/local/transport.ts +25 -5
- package/runtime/ros2_ws/src/agenticros_sim/CMakeLists.txt +25 -0
- package/runtime/ros2_ws/src/agenticros_sim/README.md +120 -0
- package/runtime/ros2_ws/src/agenticros_sim/config/agenticros-sim.config.json +28 -0
- package/runtime/ros2_ws/src/agenticros_sim/config/amr_bridge.yaml +111 -0
- package/runtime/ros2_ws/src/agenticros_sim/config/amr_view.rviz +192 -0
- package/runtime/ros2_ws/src/agenticros_sim/env-hooks/gz_resource_path.dsv.in +3 -0
- package/runtime/ros2_ws/src/agenticros_sim/env-hooks/gz_resource_path.sh.in +7 -0
- package/runtime/ros2_ws/src/agenticros_sim/launch/sim_amr.launch.py +184 -0
- package/runtime/ros2_ws/src/agenticros_sim/models/agenticros_amr/model.config +17 -0
- package/runtime/ros2_ws/src/agenticros_sim/models/agenticros_amr/model.sdf +251 -0
- package/runtime/ros2_ws/src/agenticros_sim/package.xml +27 -0
- package/runtime/ros2_ws/src/agenticros_sim/urdf/agenticros_amr.urdf.xacro +127 -0
- package/runtime/ros2_ws/src/agenticros_sim/worlds/agenticros_indoor.sdf +183 -0
- package/runtime/scripts/configure_for_sim.sh +64 -0
- package/runtime/scripts/sim/run_sim.sh +169 -0
- package/runtime/scripts/test-follow-me-sim.mjs +135 -0
- package/runtime/scripts/test-mcp-e2e.mjs +184 -0
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# scripts/sim/run_sim.sh
|
|
3
|
+
#
|
|
4
|
+
# Worker script invoked by `agenticros up sim-amr` (and later `sim-arm`). Sources
|
|
5
|
+
# ROS 2 + the AgenticROS overlay, runs `ros2 launch agenticros_sim ...`, writes
|
|
6
|
+
# its PID into /tmp/agenticros-sim.pid so `agenticros down` can stop it, and
|
|
7
|
+
# tees output to /tmp/agenticros-sim.log so `agenticros logs sim` works.
|
|
8
|
+
#
|
|
9
|
+
# Usage:
|
|
10
|
+
# run_sim.sh --robot amr [--namespace sim_robot] [--rviz] [--no-gui]
|
|
11
|
+
#
|
|
12
|
+
# Flags:
|
|
13
|
+
# --robot amr|arm Which sim launch to start (default: amr).
|
|
14
|
+
# --namespace <ns> Robot namespace; exported as AGENTICROS_ROBOT_NAMESPACE.
|
|
15
|
+
# --rviz Bring up RViz alongside Gazebo.
|
|
16
|
+
# --no-gui Run gz-sim headless (CI / docker).
|
|
17
|
+
# --ros-distro <distro> Override ROS 2 distro (auto-detect by default).
|
|
18
|
+
# --colcon-ws <path> Override the colcon workspace (default: <repo>/ros2_ws).
|
|
19
|
+
# --help Print this help and exit.
|
|
20
|
+
#
|
|
21
|
+
# Exit codes:
|
|
22
|
+
# 0 sim exited cleanly
|
|
23
|
+
# 1 missing dependency (gz, ros2, ros_gz_*, our launch file)
|
|
24
|
+
# 2 bad CLI usage
|
|
25
|
+
# 3 sim crashed at runtime — check /tmp/agenticros-sim.log
|
|
26
|
+
|
|
27
|
+
# Strict mode, but `set -u` clashes with ROS setup scripts that reference
|
|
28
|
+
# AMENT_TRACE_SETUP_FILES etc. We toggle nounset off around each `source`.
|
|
29
|
+
set -eo pipefail
|
|
30
|
+
shopt -s expand_aliases || true
|
|
31
|
+
|
|
32
|
+
# ---------- args ----------
|
|
33
|
+
ROBOT="amr"
|
|
34
|
+
NAMESPACE=""
|
|
35
|
+
USE_RVIZ="false"
|
|
36
|
+
GUI="true"
|
|
37
|
+
ROS_DISTRO_OVERRIDE=""
|
|
38
|
+
COLCON_WS=""
|
|
39
|
+
|
|
40
|
+
while [[ $# -gt 0 ]]; do
|
|
41
|
+
case "$1" in
|
|
42
|
+
--robot) ROBOT="$2"; shift 2 ;;
|
|
43
|
+
--namespace) NAMESPACE="$2"; shift 2 ;;
|
|
44
|
+
--rviz) USE_RVIZ="true"; shift ;;
|
|
45
|
+
--no-gui) GUI="false"; shift ;;
|
|
46
|
+
--ros-distro) ROS_DISTRO_OVERRIDE="$2"; shift 2 ;;
|
|
47
|
+
--colcon-ws) COLCON_WS="$2"; shift 2 ;;
|
|
48
|
+
-h|--help)
|
|
49
|
+
sed -n '2,24p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'
|
|
50
|
+
exit 0 ;;
|
|
51
|
+
*) echo "Unknown arg: $1" >&2; exit 2 ;;
|
|
52
|
+
esac
|
|
53
|
+
done
|
|
54
|
+
|
|
55
|
+
if [[ "$ROBOT" != "amr" && "$ROBOT" != "arm" ]]; then
|
|
56
|
+
echo "--robot must be 'amr' or 'arm' (got '$ROBOT')" >&2
|
|
57
|
+
exit 2
|
|
58
|
+
fi
|
|
59
|
+
|
|
60
|
+
# ---------- paths ----------
|
|
61
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
62
|
+
REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
|
63
|
+
COLCON_WS="${COLCON_WS:-$REPO_ROOT/ros2_ws}"
|
|
64
|
+
|
|
65
|
+
LOG_FILE="/tmp/agenticros-sim.log"
|
|
66
|
+
PID_FILE="/tmp/agenticros-sim.pid"
|
|
67
|
+
|
|
68
|
+
log() { printf "\033[36m[run_sim]\033[0m %s\n" "$*"; }
|
|
69
|
+
err() { printf "\033[31m[run_sim]\033[0m %s\n" "$*" >&2; }
|
|
70
|
+
|
|
71
|
+
# ---------- ROS 2 distro detection ----------
|
|
72
|
+
if [[ -n "$ROS_DISTRO_OVERRIDE" ]]; then
|
|
73
|
+
ROS_DISTRO="$ROS_DISTRO_OVERRIDE"
|
|
74
|
+
elif [[ -n "${ROS_DISTRO:-}" ]]; then
|
|
75
|
+
: # already sourced
|
|
76
|
+
else
|
|
77
|
+
for d in humble jazzy iron rolling; do
|
|
78
|
+
if [[ -f "/opt/ros/$d/setup.bash" ]]; then ROS_DISTRO="$d"; break; fi
|
|
79
|
+
done
|
|
80
|
+
fi
|
|
81
|
+
if [[ -z "${ROS_DISTRO:-}" ]] || [[ ! -f "/opt/ros/$ROS_DISTRO/setup.bash" ]]; then
|
|
82
|
+
err "ROS 2 not found under /opt/ros/. Install Humble or Jazzy first."
|
|
83
|
+
exit 1
|
|
84
|
+
fi
|
|
85
|
+
|
|
86
|
+
log "ROS_DISTRO=$ROS_DISTRO"
|
|
87
|
+
log "COLCON_WS=$COLCON_WS"
|
|
88
|
+
log "robot=$ROBOT namespace=${NAMESPACE:-<none>} rviz=$USE_RVIZ gui=$GUI"
|
|
89
|
+
|
|
90
|
+
# shellcheck disable=SC1090
|
|
91
|
+
source "/opt/ros/$ROS_DISTRO/setup.bash"
|
|
92
|
+
|
|
93
|
+
# ---------- Build agenticros_sim if not yet built ----------
|
|
94
|
+
if [[ ! -f "$COLCON_WS/install/agenticros_sim/share/agenticros_sim/launch/sim_amr.launch.py" ]]; then
|
|
95
|
+
log "agenticros_sim not built in $COLCON_WS — building now..."
|
|
96
|
+
(cd "$COLCON_WS" && colcon build --symlink-install --packages-select agenticros_sim agenticros_msgs)
|
|
97
|
+
fi
|
|
98
|
+
|
|
99
|
+
# shellcheck disable=SC1090
|
|
100
|
+
source "$COLCON_WS/install/setup.bash"
|
|
101
|
+
|
|
102
|
+
# ---------- Dependency checks ----------
|
|
103
|
+
for bin in gz ros2 rviz2; do
|
|
104
|
+
if ! command -v "$bin" >/dev/null; then
|
|
105
|
+
if [[ "$bin" == "rviz2" ]] && [[ "$USE_RVIZ" != "true" ]]; then
|
|
106
|
+
continue
|
|
107
|
+
fi
|
|
108
|
+
err "Required binary '$bin' not found on PATH."
|
|
109
|
+
exit 1
|
|
110
|
+
fi
|
|
111
|
+
done
|
|
112
|
+
|
|
113
|
+
# ---------- Wire up environment ----------
|
|
114
|
+
if [[ -n "$NAMESPACE" ]]; then
|
|
115
|
+
export AGENTICROS_ROBOT_NAMESPACE="$NAMESPACE"
|
|
116
|
+
fi
|
|
117
|
+
|
|
118
|
+
# Jetson rendering fix. On Tegra boards Mesa is picked first and tries to load
|
|
119
|
+
# nvidia-drm_dri.so which doesn't exist, so the gz GUI viewport comes up solid
|
|
120
|
+
# white. We:
|
|
121
|
+
# 1. Point libglvnd at the NVIDIA EGL vendor (works for some Jetson L4T images)
|
|
122
|
+
# 2. As a fallback, allow AGENTICROS_GZ_SOFTWARE_RENDER=1 to force llvmpipe -
|
|
123
|
+
# slow (~5 fps) but actually renders the world so the demo is viewable.
|
|
124
|
+
# Honor AGENTICROS_GZ_NO_TWEAKS=1 to skip both (useful on x86/laptops).
|
|
125
|
+
if [[ "$GUI" == "true" ]] && [[ -z "${AGENTICROS_GZ_NO_TWEAKS:-}" ]]; then
|
|
126
|
+
if [[ -f /usr/lib/aarch64-linux-gnu/tegra-egl/libEGL_nvidia.so.0 ]]; then
|
|
127
|
+
log "Jetson detected: forcing NVIDIA EGL/GL vendor"
|
|
128
|
+
export __GLX_VENDOR_LIBRARY_NAME="${__GLX_VENDOR_LIBRARY_NAME:-nvidia}"
|
|
129
|
+
export __EGL_VENDOR_LIBRARY_FILENAMES="${__EGL_VENDOR_LIBRARY_FILENAMES:-/usr/share/glvnd/egl_vendor.d/10_nvidia.json}"
|
|
130
|
+
export LD_LIBRARY_PATH="/usr/lib/aarch64-linux-gnu/tegra-egl:/usr/lib/aarch64-linux-gnu/tegra:${LD_LIBRARY_PATH:-}"
|
|
131
|
+
fi
|
|
132
|
+
if [[ -n "${AGENTICROS_GZ_SOFTWARE_RENDER:-}" ]]; then
|
|
133
|
+
log "AGENTICROS_GZ_SOFTWARE_RENDER set - forcing Mesa llvmpipe software renderer"
|
|
134
|
+
export LIBGL_ALWAYS_SOFTWARE=1
|
|
135
|
+
export GALLIUM_DRIVER=llvmpipe
|
|
136
|
+
export MESA_GL_VERSION_OVERRIDE=4.5
|
|
137
|
+
export OGRE_RTT_MODE=Copy
|
|
138
|
+
fi
|
|
139
|
+
fi
|
|
140
|
+
|
|
141
|
+
LAUNCH_ARGS=(
|
|
142
|
+
"use_rviz:=$USE_RVIZ"
|
|
143
|
+
"gui:=$GUI"
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
# ---------- Pick launch file ----------
|
|
147
|
+
case "$ROBOT" in
|
|
148
|
+
amr) LAUNCH_FILE="sim_amr.launch.py" ;;
|
|
149
|
+
arm) LAUNCH_FILE="sim_arm.launch.py" ;; # Phase 3
|
|
150
|
+
esac
|
|
151
|
+
|
|
152
|
+
if ! ros2 launch --help >/dev/null 2>&1; then
|
|
153
|
+
err "ros2 launch not available — is ROS sourced properly?"
|
|
154
|
+
exit 1
|
|
155
|
+
fi
|
|
156
|
+
|
|
157
|
+
log "Logging to $LOG_FILE"
|
|
158
|
+
log "ros2 launch agenticros_sim $LAUNCH_FILE ${LAUNCH_ARGS[*]}"
|
|
159
|
+
log "Press Ctrl+C to stop (or run \`agenticros down\` from another terminal)."
|
|
160
|
+
|
|
161
|
+
# Run in foreground so Ctrl+C in the parent shell still works, but also tee to
|
|
162
|
+
# the log so `agenticros logs sim -f` can follow concurrently. The PID we record
|
|
163
|
+
# is this script's own PID so `agenticros down` SIGTERMs the whole subtree.
|
|
164
|
+
echo "$$" > "$PID_FILE"
|
|
165
|
+
|
|
166
|
+
# Use `exec` so the ros2 process replaces our shell — that way signals from the
|
|
167
|
+
# CLI (or Ctrl+C) hit ros2 directly instead of bash.
|
|
168
|
+
exec 1> >(tee -a "$LOG_FILE") 2>&1
|
|
169
|
+
exec ros2 launch agenticros_sim "$LAUNCH_FILE" "${LAUNCH_ARGS[@]}"
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Drive ros2_follow_me_start (mode=depth) against the AMR sim.
|
|
4
|
+
*
|
|
5
|
+
* Sequence:
|
|
6
|
+
* 1. start follow-me in depth mode
|
|
7
|
+
* 2. poll status every 1.5s for 15s, capturing detection events
|
|
8
|
+
* 3. stop follow-me
|
|
9
|
+
* 4. send a zero cmd_vel just in case
|
|
10
|
+
*
|
|
11
|
+
* Reports: did the depth loop see a target, what distance/lateral did it pick,
|
|
12
|
+
* how many cmd_vel commands were issued.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { spawn } from "node:child_process";
|
|
16
|
+
import { fileURLToPath } from "node:url";
|
|
17
|
+
import { dirname, join, resolve } from "node:path";
|
|
18
|
+
|
|
19
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
20
|
+
const repoRoot = resolve(__dirname, "..");
|
|
21
|
+
const serverDist = join(repoRoot, "packages/agenticros-claude-code/dist/index.js");
|
|
22
|
+
|
|
23
|
+
const child = spawn(process.execPath, [serverDist], {
|
|
24
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
25
|
+
env: { ...process.env },
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
child.stderr.on("data", (d) => {
|
|
29
|
+
process.stderr.write(`[mcp-stderr] ${d}`);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
let nextId = 1;
|
|
33
|
+
const pending = new Map();
|
|
34
|
+
let buf = "";
|
|
35
|
+
|
|
36
|
+
child.stdout.on("data", (chunk) => {
|
|
37
|
+
buf += chunk.toString();
|
|
38
|
+
let nl;
|
|
39
|
+
while ((nl = buf.indexOf("\n")) !== -1) {
|
|
40
|
+
const line = buf.slice(0, nl).trim();
|
|
41
|
+
buf = buf.slice(nl + 1);
|
|
42
|
+
if (!line) continue;
|
|
43
|
+
let msg;
|
|
44
|
+
try {
|
|
45
|
+
msg = JSON.parse(line);
|
|
46
|
+
} catch {
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
if (msg.id !== undefined && pending.has(msg.id)) {
|
|
50
|
+
const { resolve, reject } = pending.get(msg.id);
|
|
51
|
+
pending.delete(msg.id);
|
|
52
|
+
if (msg.error) reject(new Error(msg.error.message));
|
|
53
|
+
else resolve(msg.result);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
function rpc(method, params = {}, timeoutMs = 15000) {
|
|
59
|
+
const id = nextId++;
|
|
60
|
+
return new Promise((resolveOuter, reject) => {
|
|
61
|
+
const t = setTimeout(() => {
|
|
62
|
+
pending.delete(id);
|
|
63
|
+
reject(new Error(`Timeout: ${method}`));
|
|
64
|
+
}, timeoutMs);
|
|
65
|
+
pending.set(id, {
|
|
66
|
+
resolve: (v) => {
|
|
67
|
+
clearTimeout(t);
|
|
68
|
+
resolveOuter(v);
|
|
69
|
+
},
|
|
70
|
+
reject: (e) => {
|
|
71
|
+
clearTimeout(t);
|
|
72
|
+
reject(e);
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
child.stdin.write(JSON.stringify({ jsonrpc: "2.0", id, method, params }) + "\n");
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function pickText(result) {
|
|
80
|
+
return result?.content?.map((c) => c.text ?? "").join("\n") ?? "";
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async function main() {
|
|
84
|
+
console.log("=== follow_me (depth) E2E ===");
|
|
85
|
+
|
|
86
|
+
await rpc("initialize", {
|
|
87
|
+
protocolVersion: "2024-11-05",
|
|
88
|
+
capabilities: { tools: {} },
|
|
89
|
+
clientInfo: { name: "fm-e2e", version: "0.0.1" },
|
|
90
|
+
});
|
|
91
|
+
await rpc("notifications/initialized", {}).catch(() => {});
|
|
92
|
+
|
|
93
|
+
console.log("\n-- start follow_me mode=depth --");
|
|
94
|
+
const start = await rpc("tools/call", {
|
|
95
|
+
name: "ros2_follow_me_start",
|
|
96
|
+
arguments: { mode: "depth", targetDistance: 1.5 },
|
|
97
|
+
});
|
|
98
|
+
console.log(pickText(start));
|
|
99
|
+
|
|
100
|
+
console.log("\n-- poll status for 15s --");
|
|
101
|
+
const polls = [];
|
|
102
|
+
for (let i = 0; i < 10; i++) {
|
|
103
|
+
await new Promise((r) => setTimeout(r, 1500));
|
|
104
|
+
const s = await rpc("tools/call", { name: "ros2_follow_me_status", arguments: {} });
|
|
105
|
+
const text = pickText(s);
|
|
106
|
+
polls.push({ t: (i + 1) * 1.5, text });
|
|
107
|
+
console.log(`t+${((i + 1) * 1.5).toFixed(1)}s :: ${text.replace(/\s+/g, " ").slice(0, 200)}`);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
console.log("\n-- stop follow_me --");
|
|
111
|
+
const stop = await rpc("tools/call", {
|
|
112
|
+
name: "ros2_follow_me_stop",
|
|
113
|
+
arguments: {},
|
|
114
|
+
});
|
|
115
|
+
console.log(pickText(stop));
|
|
116
|
+
|
|
117
|
+
console.log("\n-- safety zero-twist --");
|
|
118
|
+
await rpc("tools/call", {
|
|
119
|
+
name: "ros2_publish",
|
|
120
|
+
arguments: {
|
|
121
|
+
topic: "/cmd_vel",
|
|
122
|
+
type: "geometry_msgs/msg/Twist",
|
|
123
|
+
message: { linear: { x: 0, y: 0, z: 0 }, angular: { x: 0, y: 0, z: 0 } },
|
|
124
|
+
},
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
child.kill("SIGTERM");
|
|
128
|
+
console.log("\n=== Done ===");
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
main().catch((e) => {
|
|
132
|
+
console.error("Failed:", e);
|
|
133
|
+
child.kill("SIGKILL");
|
|
134
|
+
process.exit(1);
|
|
135
|
+
});
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Spin up the @agenticros/claude-code MCP server as a child process and exercise
|
|
4
|
+
* a real JSON-RPC session over stdio. Used for end-to-end testing of the MCP
|
|
5
|
+
* tools against a live sim (or real robot).
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* node scripts/test-mcp-e2e.mjs
|
|
9
|
+
*
|
|
10
|
+
* Honours AGENTICROS_CONFIG_PATH if set; otherwise uses ~/.agenticros/config.json.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { spawn } from "node:child_process";
|
|
14
|
+
import { fileURLToPath } from "node:url";
|
|
15
|
+
import { dirname, join, resolve } from "node:path";
|
|
16
|
+
|
|
17
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
18
|
+
const repoRoot = resolve(__dirname, "..");
|
|
19
|
+
const serverDist = join(repoRoot, "packages/agenticros-claude-code/dist/index.js");
|
|
20
|
+
|
|
21
|
+
const child = spawn(process.execPath, [serverDist], {
|
|
22
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
23
|
+
env: { ...process.env },
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
child.stderr.on("data", (d) => {
|
|
27
|
+
process.stderr.write(`[mcp-stderr] ${d}`);
|
|
28
|
+
});
|
|
29
|
+
child.on("exit", (code, sig) => {
|
|
30
|
+
process.stderr.write(`[mcp] exited code=${code} sig=${sig}\n`);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
let nextId = 1;
|
|
34
|
+
const pending = new Map();
|
|
35
|
+
let buf = "";
|
|
36
|
+
|
|
37
|
+
child.stdout.on("data", (chunk) => {
|
|
38
|
+
buf += chunk.toString();
|
|
39
|
+
let nl;
|
|
40
|
+
while ((nl = buf.indexOf("\n")) !== -1) {
|
|
41
|
+
const line = buf.slice(0, nl).trim();
|
|
42
|
+
buf = buf.slice(nl + 1);
|
|
43
|
+
if (!line) continue;
|
|
44
|
+
let msg;
|
|
45
|
+
try {
|
|
46
|
+
msg = JSON.parse(line);
|
|
47
|
+
} catch (e) {
|
|
48
|
+
process.stderr.write(`[mcp] non-json line: ${line}\n`);
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
if (msg.id !== undefined && pending.has(msg.id)) {
|
|
52
|
+
const { resolve, reject } = pending.get(msg.id);
|
|
53
|
+
pending.delete(msg.id);
|
|
54
|
+
if (msg.error) reject(new Error(`${msg.error.code}: ${msg.error.message}`));
|
|
55
|
+
else resolve(msg.result);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
function rpc(method, params = {}, timeoutMs = 20000) {
|
|
61
|
+
const id = nextId++;
|
|
62
|
+
const payload = { jsonrpc: "2.0", id, method, params };
|
|
63
|
+
return new Promise((resolveOuter, reject) => {
|
|
64
|
+
const t = setTimeout(() => {
|
|
65
|
+
pending.delete(id);
|
|
66
|
+
reject(new Error(`Timeout: ${method}`));
|
|
67
|
+
}, timeoutMs);
|
|
68
|
+
pending.set(id, {
|
|
69
|
+
resolve: (v) => {
|
|
70
|
+
clearTimeout(t);
|
|
71
|
+
resolveOuter(v);
|
|
72
|
+
},
|
|
73
|
+
reject: (e) => {
|
|
74
|
+
clearTimeout(t);
|
|
75
|
+
reject(e);
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
child.stdin.write(JSON.stringify(payload) + "\n");
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function summarise(content, max = 4000) {
|
|
83
|
+
if (!Array.isArray(content)) return JSON.stringify(content).slice(0, max);
|
|
84
|
+
return content
|
|
85
|
+
.map((c) => {
|
|
86
|
+
if (c.type === "text") {
|
|
87
|
+
const t = c.text ?? "";
|
|
88
|
+
return t.length > max ? `${t.slice(0, max)} … (+${t.length - max} chars)` : t;
|
|
89
|
+
}
|
|
90
|
+
if (c.type === "image") return `[image base64 len=${(c.data ?? "").length} mime=${c.mimeType}]`;
|
|
91
|
+
return `[${c.type}]`;
|
|
92
|
+
})
|
|
93
|
+
.join("\n");
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async function callTool(name, args = {}, timeoutMs = 20000) {
|
|
97
|
+
const t0 = Date.now();
|
|
98
|
+
try {
|
|
99
|
+
const result = await rpc("tools/call", { name, arguments: args }, timeoutMs);
|
|
100
|
+
const ms = Date.now() - t0;
|
|
101
|
+
const ok = result?.isError ? "ERR" : "ok ";
|
|
102
|
+
console.log(`[${ok}] (${ms.toString().padStart(5)}ms) ${name}`);
|
|
103
|
+
console.log(` ${summarise(result?.content)}`);
|
|
104
|
+
return result;
|
|
105
|
+
} catch (e) {
|
|
106
|
+
const ms = Date.now() - t0;
|
|
107
|
+
console.log(`[ERR] (${ms.toString().padStart(5)}ms) ${name} ${e.message}`);
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async function main() {
|
|
113
|
+
console.log("=== MCP E2E harness ===");
|
|
114
|
+
|
|
115
|
+
console.log("\n-- initialize --");
|
|
116
|
+
const init = await rpc("initialize", {
|
|
117
|
+
protocolVersion: "2024-11-05",
|
|
118
|
+
capabilities: { tools: {} },
|
|
119
|
+
clientInfo: { name: "agenticros-e2e", version: "0.0.1" },
|
|
120
|
+
});
|
|
121
|
+
console.log(`server: ${init.serverInfo?.name} v${init.serverInfo?.version}`);
|
|
122
|
+
await rpc("notifications/initialized", {}).catch(() => {});
|
|
123
|
+
|
|
124
|
+
console.log("\n-- tools/list --");
|
|
125
|
+
const tl = await rpc("tools/list", {});
|
|
126
|
+
console.log(` ${tl.tools?.length ?? 0} tools advertised`);
|
|
127
|
+
for (const t of tl.tools ?? []) {
|
|
128
|
+
console.log(` - ${t.name}`);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
console.log("\n-- ros2_list_topics --");
|
|
132
|
+
await callTool("ros2_list_topics", {}, 30000);
|
|
133
|
+
|
|
134
|
+
console.log("\n-- ros2_publish /cmd_vel (linear.x=0.2) --");
|
|
135
|
+
await callTool(
|
|
136
|
+
"ros2_publish",
|
|
137
|
+
{
|
|
138
|
+
topic: "/cmd_vel",
|
|
139
|
+
type: "geometry_msgs/msg/Twist",
|
|
140
|
+
message: { linear: { x: 0.2, y: 0, z: 0 }, angular: { x: 0, y: 0, z: 0 } },
|
|
141
|
+
},
|
|
142
|
+
10000,
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
console.log("\n-- ros2_subscribe_once /imu/data --");
|
|
146
|
+
await callTool(
|
|
147
|
+
"ros2_subscribe_once",
|
|
148
|
+
{ topic: "/imu/data", type: "sensor_msgs/msg/Imu", timeoutMs: 5000 },
|
|
149
|
+
10000,
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
console.log("\n-- ros2_subscribe_once /scan --");
|
|
153
|
+
await callTool(
|
|
154
|
+
"ros2_subscribe_once",
|
|
155
|
+
{ topic: "/scan", type: "sensor_msgs/msg/LaserScan", timeoutMs: 5000 },
|
|
156
|
+
10000,
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
console.log("\n-- ros2_camera_snapshot (RGB) --");
|
|
160
|
+
await callTool("ros2_camera_snapshot", {}, 15000);
|
|
161
|
+
|
|
162
|
+
console.log("\n-- ros2_depth_distance --");
|
|
163
|
+
await callTool("ros2_depth_distance", {}, 15000);
|
|
164
|
+
|
|
165
|
+
console.log("\n-- stop --");
|
|
166
|
+
await callTool(
|
|
167
|
+
"ros2_publish",
|
|
168
|
+
{
|
|
169
|
+
topic: "/cmd_vel",
|
|
170
|
+
type: "geometry_msgs/msg/Twist",
|
|
171
|
+
message: { linear: { x: 0, y: 0, z: 0 }, angular: { x: 0, y: 0, z: 0 } },
|
|
172
|
+
},
|
|
173
|
+
5000,
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
console.log("\n=== Done ===");
|
|
177
|
+
child.kill("SIGTERM");
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
main().catch((e) => {
|
|
181
|
+
console.error("Harness failed:", e);
|
|
182
|
+
child.kill("SIGKILL");
|
|
183
|
+
process.exit(1);
|
|
184
|
+
});
|