Skip to content

Commit 7483c77

Browse files
committed
feat(cursor-cli): add project MCP and rules support; non-interactive only
- mcp_json -> <folder>/.cursor/mcp.json (optional) - rules_files map -> <folder>/.cursor/rules/* (optional); link rules docs - always -p; default output_format=json; README updates
1 parent a8d9b71 commit 7483c77

File tree

5 files changed

+125
-63
lines changed

5 files changed

+125
-63
lines changed

registry/coder-labs/modules/cursor-cli/README.md

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,9 @@ tags: [agent, cursor, ai, cli]
1010

1111
Run the Cursor Coding Agent in your workspace using the Cursor CLI directly. This module does not use AgentAPI and executes the Cursor agent process itself.
1212

13-
- Defaults to interactive mode, with an option for non-interactive mode
13+
- Runs non-interactive (autonomous) by default, using `-p` (print)
1414
- Supports `--force` runs
15-
- Allows configuring MCP servers (settings merge)
15+
- Allows configuring MCP servers and project MCP (`~/.cursor/settings.json` and `<folder>/.cursor/mcp.json`)
1616
- Lets you choose a model and pass extra CLI arguments
1717

1818
```tf
@@ -25,10 +25,15 @@ module "cursor_cli" {
2525
folder = "/home/coder/project"
2626
install_cursor_cli = true
2727
cursor_cli_version = "latest"
28-
interactive = true
29-
non_interactive_cmd = "run --once"
28+
base_command = "status" # optional subcommand (default is chat mode)
29+
output_format = "json" # text | json | stream-json
3030
force = false
31-
model = "gpt-4o"
31+
model = "gpt-5"
32+
mcp_json = jsonencode({
33+
mcpServers = {
34+
# example project-specific servers (see docs)
35+
}
36+
})
3237
additional_settings = jsonencode({
3338
mcpServers = {
3439
coder = {
@@ -48,8 +53,9 @@ module "cursor_cli" {
4853
## Notes
4954

5055
- See Cursor CLI docs: `https://docs.cursor.com/en/cli/overview`
51-
- The module writes merged settings to `~/.cursor/settings.json`
52-
- Interactive by default; set `interactive = false` to run non-interactively via `non_interactive_cmd`
56+
- For MCP project config, see `https://docs.cursor.com/en/context/mcp#using-mcp-json`. This module writes your `mcp_json` into `<folder>/.cursor/mcp.json` and merges `additional_settings` into `~/.cursor/settings.json`.
57+
- For Rules, see `https://docs.cursor.com/en/context/rules#project-rules`. Provide `rules_files` (map of file name to content) to populate `<folder>/.cursor/rules/`.
58+
- The agent runs non-interactively with `-p` by default. Use `output_format` to choose `text | json | stream-json` (default `json`).
5359

5460
## Troubleshooting
5561

registry/coder-labs/modules/cursor-cli/cursor-cli.tftest.hcl

Lines changed: 62 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,13 @@
11
// Terraform tests for the cursor-cli module
22
// Validates that we render expected script content given inputs
33

4-
run "defaults_interactive" {
4+
run "defaults_noninteractive" {
55
command = plan
66

77
variables {
88
agent_id = "test-agent"
99
}
1010

11-
assert {
12-
condition = can(regex("INTERACTIVE='true'", resource.coder_script.cursor_cli.script))
13-
error_message = "Expected INTERACTIVE default to be true"
14-
}
15-
1611
assert {
1712
condition = can(regex("BINARY_NAME='cursor-agent'", resource.coder_script.cursor_cli.script))
1813
error_message = "Expected default binary_name to be cursor-agent"
@@ -23,19 +18,16 @@ run "non_interactive_mode" {
2318
command = plan
2419

2520
variables {
26-
agent_id = "test-agent"
27-
interactive = false
28-
non_interactive_cmd = "run --once"
21+
agent_id = "test-agent"
22+
base_command = "status"
23+
extra_args = ["--dry-run"]
24+
output_format = "json"
2925
}
3026

3127
assert {
32-
condition = can(regex("INTERACTIVE='false'", resource.coder_script.cursor_cli.script))
33-
error_message = "Expected INTERACTIVE to be false when interactive=false"
34-
}
35-
36-
assert {
37-
condition = can(regex("NON_INTERACTIVE_CMD='run --once'", resource.coder_script.cursor_cli.script))
38-
error_message = "Expected NON_INTERACTIVE_CMD to be propagated"
28+
// base command and -p --output-format json are included in env
29+
condition = can(regex("BASE_COMMAND='status'", resource.coder_script.cursor_cli.script))
30+
error_message = "Expected BASE_COMMAND to be propagated"
3931
}
4032
}
4133

@@ -73,6 +65,10 @@ run "additional_settings_propagated" {
7365
}
7466
}
7567
})
68+
mcp_json = jsonencode({ mcpServers = { foo = { command = "foo", type = "stdio" } } })
69+
rules_files = {
70+
"global.yml" = "version: 1\nrules:\n - name: global\n include: ['**/*']\n description: global rule"
71+
}
7672
}
7773

7874
// Ensure the encoded settings are passed into the install invocation
@@ -88,4 +84,54 @@ run "additional_settings_propagated" {
8884
})), resource.coder_script.cursor_cli.script))
8985
error_message = "Expected ADDITIONAL_SETTINGS (base64) to be in the install step"
9086
}
87+
88+
// Ensure project mcp_json is passed
89+
assert {
90+
condition = can(regex(base64encode(jsonencode({ mcpServers = { foo = { command = "foo", type = "stdio" } } })), resource.coder_script.cursor_cli.script))
91+
error_message = "Expected PROJECT_MCP_JSON (base64) to be in the install step"
92+
}
93+
94+
// Ensure rules map is passed
95+
assert {
96+
condition = can(regex(base64encode(jsonencode({"global.yml":"version: 1\nrules:\n - name: global\n include: ['**/*']\n description: global rule"})), resource.coder_script.cursor_cli.script))
97+
error_message = "Expected PROJECT_RULES_JSON (base64) to be in the install step"
98+
}
99+
}
100+
101+
run "output_api_key_binary_basecmd_extra" {
102+
command = plan
103+
104+
variables {
105+
agent_id = "test-agent"
106+
output_format = "json"
107+
api_key = "sk-test-123"
108+
binary_name = "cursor-agent"
109+
base_command = "status"
110+
extra_args = ["--foo", "bar"]
111+
}
112+
113+
assert {
114+
condition = can(regex("OUTPUT_FORMAT='json'", resource.coder_script.cursor_cli.script))
115+
error_message = "Expected output format to be passed"
116+
}
117+
118+
assert {
119+
condition = can(regex("API_KEY_SECRET='sk-test-123'", resource.coder_script.cursor_cli.script))
120+
error_message = "Expected API key to be plumbed (to CURSOR_API_KEY at runtime)"
121+
}
122+
123+
assert {
124+
condition = can(regex("BINARY_NAME='cursor-agent'", resource.coder_script.cursor_cli.script))
125+
error_message = "Expected binary name to be forwarded"
126+
}
127+
128+
assert {
129+
condition = can(regex("BASE_COMMAND='status'", resource.coder_script.cursor_cli.script))
130+
error_message = "Expected base command to be forwarded"
131+
}
132+
133+
assert {
134+
condition = can(regex(base64encode("--foo\nbar"), resource.coder_script.cursor_cli.script))
135+
error_message = "Expected extra args to be base64 encoded and passed"
136+
}
91137
}

registry/coder-labs/modules/cursor-cli/main.tf

Lines changed: 16 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -54,23 +54,8 @@ variable "cursor_cli_version" {
5454
default = "latest"
5555
}
5656

57-
variable "interactive" {
58-
type = bool
59-
description = "Run in interactive chat mode (default)."
60-
default = true
61-
}
57+
# Running mode is non-interactive by design for automation.
6258

63-
variable "initial_prompt" {
64-
type = string
65-
description = "Initial prompt to start the chat with (passed as trailing arg)."
66-
default = ""
67-
}
68-
69-
variable "non_interactive_cmd" {
70-
type = string
71-
description = "Additional arguments appended when interactive=false (advanced usage)."
72-
default = ""
73-
}
7459

7560
variable "force" {
7661
type = bool
@@ -121,6 +106,18 @@ variable "additional_settings" {
121106
default = ""
122107
}
123108

109+
variable "mcp_json" {
110+
type = string
111+
description = "Project-specific MCP JSON to write to <folder>/.cursor/mcp.json. See https://docs.cursor.com/en/context/mcp#using-mcp-json"
112+
default = null
113+
}
114+
115+
variable "rules_files" {
116+
type = map(string)
117+
description = "Optional map of rule file name to content. Files will be written to <folder>/.cursor/rules/<name>. See https://docs.cursor.com/en/context/rules#project-rules"
118+
default = null
119+
}
120+
124121
locals {
125122
app_slug = "cursor-cli"
126123
install_script = file("${path.module}/scripts/install.sh")
@@ -142,15 +139,15 @@ resource "coder_script" "cursor_cli" {
142139
ARG_INSTALL='${var.install_cursor_cli}' \
143140
ARG_VERSION='${var.cursor_cli_version}' \
144141
ADDITIONAL_SETTINGS='${base64encode(replace(var.additional_settings, "'", "'\\''"))}' \
142+
PROJECT_MCP_JSON='${var.mcp_json != null ? base64encode(replace(var.mcp_json, "'", "'\\''")) : ""}' \
143+
PROJECT_RULES_JSON='${var.rules_files != null ? base64encode(jsonencode(var.rules_files)) : ""}' \
145144
MODULE_DIR_NAME='${local.module_dir_name}' \
146145
FOLDER='${var.folder}' \
147146
/tmp/install.sh | tee "$HOME/${local.module_dir_name}/install.log"
148147
149148
echo -n '${base64encode(local.start_script)}' | base64 -d > /tmp/start.sh
150149
chmod +x /tmp/start.sh
151-
INTERACTIVE='${var.interactive}' \
152-
INITIAL_PROMPT='${replace(var.initial_prompt, "'", "'\\''")}' \
153-
NON_INTERACTIVE_CMD='${replace(var.non_interactive_cmd, "'", "'\\''")}' \
150+
# Non-interactive mode by design
154151
BASE_COMMAND='${var.base_command}' \
155152
FORCE='${var.force}' \
156153
MODEL='${var.model}' \

registry/coder-labs/modules/cursor-cli/scripts/install.sh

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ FOLDER=${FOLDER:-$HOME}
1616
mkdir -p "$HOME/$MODULE_DIR_NAME"
1717

1818
ADDITIONAL_SETTINGS=$(echo -n "$ADDITIONAL_SETTINGS" | base64 -d)
19+
PROJECT_MCP_JSON=$(echo -n "$PROJECT_MCP_JSON" | base64 -d)
20+
PROJECT_RULES_JSON=$(echo -n "$PROJECT_RULES_JSON" | base64 -d)
1921

2022
{
2123
echo "--------------------------------"
@@ -76,6 +78,27 @@ fi
7678
SETTINGS_PATH="$HOME/.cursor/settings.json"
7779
mkdir -p "$(dirname "$SETTINGS_PATH")"
7880

81+
# Write project-specific MCP if provided
82+
if [ -n "$PROJECT_MCP_JSON" ]; then
83+
TARGET_DIR="$FOLDER/.cursor"
84+
mkdir -p "$TARGET_DIR"
85+
echo "$PROJECT_MCP_JSON" > "$TARGET_DIR/mcp.json"
86+
echo "Wrote project MCP to $TARGET_DIR/mcp.json" | tee -a "$HOME/$MODULE_DIR_NAME/install.log"
87+
fi
88+
89+
# Write rules files if provided (map of name->content)
90+
if [ -n "$PROJECT_RULES_JSON" ]; then
91+
RULES_DIR="$FOLDER/.cursor/rules"
92+
mkdir -p "$RULES_DIR"
93+
echo "$PROJECT_RULES_JSON" | jq -r 'to_entries[] | @base64' | while read -r entry; do
94+
_jq() { echo "${entry}" | base64 -d | jq -r ${1}; }
95+
NAME=$(_jq '.key')
96+
CONTENT=$(_jq '.value')
97+
echo "$CONTENT" > "$RULES_DIR/$NAME"
98+
echo "Wrote rule: $RULES_DIR/$NAME" | tee -a "$HOME/$MODULE_DIR_NAME/install.log"
99+
done
100+
fi
101+
79102
# If settings file doesn't exist, initialize basic structure
80103
if [ ! -f "$SETTINGS_PATH" ]; then
81104
echo '{}' > "$SETTINGS_PATH"

registry/coder-labs/modules/cursor-cli/scripts/start.sh

Lines changed: 11 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,11 @@ command_exists() {
77
command -v "$1" >/dev/null 2>&1
88
}
99

10-
INTERACTIVE=${INTERACTIVE:-true}
11-
INITIAL_PROMPT=${INITIAL_PROMPT:-}
10+
# Non-interactive autonomous mode only
1211
NON_INTERACTIVE_CMD=${NON_INTERACTIVE_CMD:-}
1312
FORCE=${FORCE:-false}
1413
MODEL=${MODEL:-}
15-
OUTPUT_FORMAT=${OUTPUT_FORMAT:-}
14+
OUTPUT_FORMAT=${OUTPUT_FORMAT:-json}
1615
API_KEY_SECRET=${API_KEY_SECRET:-}
1716
EXTRA_ARGS_BASE64=${EXTRA_ARGS:-}
1817
MODULE_DIR_NAME=${MODULE_DIR_NAME:-.cursor-cli-module}
@@ -57,31 +56,22 @@ if [ "$FORCE" = "true" ]; then
5756
ARGS+=("-f")
5857
fi
5958

60-
# Non-interactive printing flags
61-
PRINT_TO_CONSOLE=false
62-
if [ "$INTERACTIVE" != "true" ]; then
63-
PRINT_TO_CONSOLE=true
64-
ARGS+=("-p")
65-
if [ -n "$OUTPUT_FORMAT" ]; then
66-
ARGS+=("--output-format" "$OUTPUT_FORMAT")
67-
fi
68-
if [ -n "$NON_INTERACTIVE_CMD" ]; then
69-
# shellcheck disable=SC2206
70-
CMD_PARTS=($NON_INTERACTIVE_CMD)
71-
ARGS+=("${CMD_PARTS[@]}")
72-
fi
59+
# Non-interactive printing flags (always enabled)
60+
ARGS+=("-p")
61+
if [ -n "$OUTPUT_FORMAT" ]; then
62+
ARGS+=("--output-format" "$OUTPUT_FORMAT")
63+
fi
64+
if [ -n "$NON_INTERACTIVE_CMD" ]; then
65+
# shellcheck disable=SC2206
66+
CMD_PARTS=($NON_INTERACTIVE_CMD)
67+
ARGS+=("${CMD_PARTS[@]}")
7368
fi
7469

7570
# Extra args, if any
7671
if [ ${#EXTRA_ARR[@]} -gt 0 ]; then
7772
ARGS+=("${EXTRA_ARR[@]}")
7873
fi
7974

80-
# If initial prompt specified (chat mode), pass as trailing arg
81-
if [ -n "$INITIAL_PROMPT" ]; then
82-
ARGS+=("$INITIAL_PROMPT")
83-
fi
84-
8575
# Set API key env if provided
8676
if [ -n "$API_KEY_SECRET" ]; then
8777
export CURSOR_API_KEY="$API_KEY_SECRET"

0 commit comments

Comments
 (0)