agent: Use standardized MCP configuration format in settings (#33539)

Changes our MCP settings from:

```json
{
  "context_servers": {
    "some-mcp-server": {
      "source": "custom",
      "command": {
        "path": "npx",
        "args": [
          "-y",
          "@supabase/mcp-server-supabase@latest",
          "--read-only",
          "--project-ref=<project-ref>",
        ],
        "env": {
          "SUPABASE_ACCESS_TOKEN": "<personal-access-token>",
        },
      },
    },
  },
}

```

to:
```json
{
  "context_servers": {
    "some-mcp-server": {
      "source": "custom",
      "command": "npx",
      "args": [
        "-y",
        "@supabase/mcp-server-supabase@latest",
        "--read-only",
        "--project-ref=<project-ref>",
      ],
      "env": {
        "SUPABASE_ACCESS_TOKEN": "<personal-access-token>",
      },
    },
  },
}
```


Which seems to be somewhat of a standard now (VSCode, Cursor, Windsurf,
...)

Release Notes:

- agent: Use standardised format for configuring MCP Servers
This commit is contained in:
Bennet Bo Fenner 2025-06-30 10:05:52 +02:00 committed by GitHub
parent c3d0230f89
commit d63909c598
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 276 additions and 16 deletions

View file

@ -87,3 +87,9 @@ pub(crate) mod m_2025_06_25 {
pub(crate) use settings::SETTINGS_PATTERNS;
}
pub(crate) mod m_2025_06_27 {
mod settings;
pub(crate) use settings::SETTINGS_PATTERNS;
}

View file

@ -0,0 +1,133 @@
use std::ops::Range;
use tree_sitter::{Query, QueryMatch};
use crate::MigrationPatterns;
pub const SETTINGS_PATTERNS: MigrationPatterns = &[(
SETTINGS_CONTEXT_SERVER_PATTERN,
flatten_context_server_command,
)];
const SETTINGS_CONTEXT_SERVER_PATTERN: &str = r#"(document
(object
(pair
key: (string (string_content) @context-servers)
value: (object
(pair
key: (string (string_content) @server-name)
value: (object
(pair
key: (string (string_content) @source-key)
value: (string (string_content) @source-value)
)
(pair
key: (string (string_content) @command-key)
value: (object) @command-object
) @command-pair
) @server-settings
)
)
)
)
(#eq? @context-servers "context_servers")
(#eq? @source-key "source")
(#eq? @source-value "custom")
(#eq? @command-key "command")
)"#;
fn flatten_context_server_command(
contents: &str,
mat: &QueryMatch,
query: &Query,
) -> Option<(Range<usize>, String)> {
let command_pair_index = query.capture_index_for_name("command-pair")?;
let command_pair = mat.nodes_for_capture_index(command_pair_index).next()?;
let command_object_index = query.capture_index_for_name("command-object")?;
let command_object = mat.nodes_for_capture_index(command_object_index).next()?;
let server_settings_index = query.capture_index_for_name("server-settings")?;
let _server_settings = mat.nodes_for_capture_index(server_settings_index).next()?;
// Parse the command object to extract path, args, and env
let mut path_value = None;
let mut args_value = None;
let mut env_value = None;
let mut cursor = command_object.walk();
for child in command_object.children(&mut cursor) {
if child.kind() == "pair" {
if let Some(key_node) = child.child_by_field_name("key") {
if let Some(string_content) = key_node.child(1) {
let key = &contents[string_content.byte_range()];
if let Some(value_node) = child.child_by_field_name("value") {
let value_range = value_node.byte_range();
match key {
"path" => path_value = Some(&contents[value_range]),
"args" => args_value = Some(&contents[value_range]),
"env" => env_value = Some(&contents[value_range]),
_ => {}
}
}
}
}
}
}
let path = path_value?;
// Get the proper indentation from the command pair
let command_pair_start = command_pair.start_byte();
let line_start = contents[..command_pair_start]
.rfind('\n')
.map(|pos| pos + 1)
.unwrap_or(0);
let indent = &contents[line_start..command_pair_start];
// Build the replacement string
let mut replacement = format!("\"command\": {}", path);
// Add args if present - need to reduce indentation
if let Some(args) = args_value {
replacement.push_str(",\n");
replacement.push_str(indent);
replacement.push_str("\"args\": ");
let reduced_args = reduce_indentation(args, 4);
replacement.push_str(&reduced_args);
}
// Add env if present - need to reduce indentation
if let Some(env) = env_value {
replacement.push_str(",\n");
replacement.push_str(indent);
replacement.push_str("\"env\": ");
replacement.push_str(&reduce_indentation(env, 4));
}
let range_to_replace = command_pair.byte_range();
Some((range_to_replace, replacement))
}
fn reduce_indentation(text: &str, spaces: usize) -> String {
let lines: Vec<&str> = text.lines().collect();
let mut result = String::new();
for (i, line) in lines.iter().enumerate() {
if i > 0 {
result.push('\n');
}
// Count leading spaces
let leading_spaces = line.chars().take_while(|&c| c == ' ').count();
if leading_spaces >= spaces {
// Reduce indentation
result.push_str(&line[spaces..]);
} else {
// Keep line as is if it doesn't have enough indentation
result.push_str(line);
}
}
result
}

View file

@ -156,6 +156,10 @@ pub fn migrate_settings(text: &str) -> Result<Option<String>> {
migrations::m_2025_06_25::SETTINGS_PATTERNS,
&SETTINGS_QUERY_2025_06_25,
),
(
migrations::m_2025_06_27::SETTINGS_PATTERNS,
&SETTINGS_QUERY_2025_06_27,
),
];
run_migrations(text, migrations)
}
@ -262,6 +266,10 @@ define_query!(
SETTINGS_QUERY_2025_06_25,
migrations::m_2025_06_25::SETTINGS_PATTERNS
);
define_query!(
SETTINGS_QUERY_2025_06_27,
migrations::m_2025_06_27::SETTINGS_PATTERNS
);
// custom query
static EDIT_PREDICTION_SETTINGS_MIGRATION_QUERY: LazyLock<Query> = LazyLock::new(|| {
@ -286,6 +294,15 @@ mod tests {
pretty_assertions::assert_eq!(migrated.as_deref(), output);
}
fn assert_migrate_settings_with_migrations(
migrations: &[(MigrationPatterns, &Query)],
input: &str,
output: Option<&str>,
) {
let migrated = run_migrations(input, migrations).unwrap();
pretty_assertions::assert_eq!(migrated.as_deref(), output);
}
#[test]
fn test_replace_array_with_single_string() {
assert_migrate_keymap(
@ -873,7 +890,11 @@ mod tests {
#[test]
fn test_mcp_settings_migration() {
assert_migrate_settings(
assert_migrate_settings_with_migrations(
&[(
migrations::m_2025_06_16::SETTINGS_PATTERNS,
&SETTINGS_QUERY_2025_06_16,
)],
r#"{
"context_servers": {
"empty_server": {},
@ -1058,7 +1079,14 @@ mod tests {
}
}
}"#;
assert_migrate_settings(settings, None);
assert_migrate_settings_with_migrations(
&[(
migrations::m_2025_06_16::SETTINGS_PATTERNS,
&SETTINGS_QUERY_2025_06_16,
)],
settings,
None,
);
}
#[test]
@ -1131,4 +1159,100 @@ mod tests {
None,
);
}
#[test]
fn test_flatten_context_server_command() {
assert_migrate_settings(
r#"{
"context_servers": {
"some-mcp-server": {
"source": "custom",
"command": {
"path": "npx",
"args": [
"-y",
"@supabase/mcp-server-supabase@latest",
"--read-only",
"--project-ref=<project-ref>"
],
"env": {
"SUPABASE_ACCESS_TOKEN": "<personal-access-token>"
}
}
}
}
}"#,
Some(
r#"{
"context_servers": {
"some-mcp-server": {
"source": "custom",
"command": "npx",
"args": [
"-y",
"@supabase/mcp-server-supabase@latest",
"--read-only",
"--project-ref=<project-ref>"
],
"env": {
"SUPABASE_ACCESS_TOKEN": "<personal-access-token>"
}
}
}
}"#,
),
);
// Test with additional keys in server object
assert_migrate_settings(
r#"{
"context_servers": {
"server-with-extras": {
"source": "custom",
"command": {
"path": "/usr/bin/node",
"args": ["server.js"]
},
"settings": {}
}
}
}"#,
Some(
r#"{
"context_servers": {
"server-with-extras": {
"source": "custom",
"command": "/usr/bin/node",
"args": ["server.js"],
"settings": {}
}
}
}"#,
),
);
// Test command without args or env
assert_migrate_settings(
r#"{
"context_servers": {
"simple-server": {
"source": "custom",
"command": {
"path": "simple-mcp-server"
}
}
}
}"#,
Some(
r#"{
"context_servers": {
"simple-server": {
"source": "custom",
"command": "simple-mcp-server"
}
}
}"#,
),
);
}
}