feat: one click installation for popular MCP servers (#657)
* backend configuration * frontend * fmt * adapt remote config * lock * opencode adapter
This commit is contained in:
committed by
GitHub
parent
c79f0a200d
commit
4c5be4e807
43
crates/executors/default_mcp.json
Normal file
43
crates/executors/default_mcp.json
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
{
|
||||||
|
"vibe_kanban": {
|
||||||
|
"command": "npx",
|
||||||
|
"args": [
|
||||||
|
"-y",
|
||||||
|
"vibe-kanban",
|
||||||
|
"--mcp"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"context7": {
|
||||||
|
"type": "http",
|
||||||
|
"url": "https://mcp.context7.com/mcp",
|
||||||
|
"headers": {
|
||||||
|
"CONTEXT7_API_KEY": "YOUR_API_KEY"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"playwright": {
|
||||||
|
"command": "npx",
|
||||||
|
"args": [
|
||||||
|
"@playwright/mcp@latest"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"meta": {
|
||||||
|
"vibe_kanban": {
|
||||||
|
"name": "Vibe Kanban",
|
||||||
|
"description": "Create, update and delete Vibe Kanban tasks",
|
||||||
|
"url": "https://www.vibekanban.com/docs/user-guide/vibe-kanban-mcp-server",
|
||||||
|
"icon": "viba-kanban-favicon.png"
|
||||||
|
},
|
||||||
|
"context7": {
|
||||||
|
"name": "Context7",
|
||||||
|
"description": "Fetch up-to-date documentation and code examples",
|
||||||
|
"url": "https://github.com/upstash/context7",
|
||||||
|
"icon": "mcp/context7logo.png"
|
||||||
|
},
|
||||||
|
"playwright": {
|
||||||
|
"name": "Playwright",
|
||||||
|
"description": "Browser automation with Playwright",
|
||||||
|
"url": "https://github.com/microsoft/playwright-mcp",
|
||||||
|
"icon": "mcp/playwright_logo_icon.svg"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -85,10 +85,7 @@ impl CodingAgent {
|
|||||||
serde_json::json!({
|
serde_json::json!({
|
||||||
"mcp_servers": {}
|
"mcp_servers": {}
|
||||||
}),
|
}),
|
||||||
serde_json::json!({
|
self.preconfigured_mcp(),
|
||||||
"command": "npx",
|
|
||||||
"args": ["-y", "vibe-kanban", "--mcp"],
|
|
||||||
}),
|
|
||||||
true,
|
true,
|
||||||
),
|
),
|
||||||
Self::Amp(_) => McpConfig::new(
|
Self::Amp(_) => McpConfig::new(
|
||||||
@@ -96,10 +93,7 @@ impl CodingAgent {
|
|||||||
serde_json::json!({
|
serde_json::json!({
|
||||||
"amp.mcpServers": {}
|
"amp.mcpServers": {}
|
||||||
}),
|
}),
|
||||||
serde_json::json!({
|
self.preconfigured_mcp(),
|
||||||
"command": "npx",
|
|
||||||
"args": ["-y", "vibe-kanban", "--mcp"],
|
|
||||||
}),
|
|
||||||
false,
|
false,
|
||||||
),
|
),
|
||||||
Self::Opencode(_) => McpConfig::new(
|
Self::Opencode(_) => McpConfig::new(
|
||||||
@@ -108,11 +102,7 @@ impl CodingAgent {
|
|||||||
"mcp": {},
|
"mcp": {},
|
||||||
"$schema": "https://opencode.ai/config.json"
|
"$schema": "https://opencode.ai/config.json"
|
||||||
}),
|
}),
|
||||||
serde_json::json!({
|
self.preconfigured_mcp(),
|
||||||
"type": "local",
|
|
||||||
"command": ["npx", "-y", "vibe-kanban", "--mcp"],
|
|
||||||
"enabled": true
|
|
||||||
}),
|
|
||||||
false,
|
false,
|
||||||
),
|
),
|
||||||
_ => McpConfig::new(
|
_ => McpConfig::new(
|
||||||
@@ -120,10 +110,7 @@ impl CodingAgent {
|
|||||||
serde_json::json!({
|
serde_json::json!({
|
||||||
"mcpServers": {}
|
"mcpServers": {}
|
||||||
}),
|
}),
|
||||||
serde_json::json!({
|
self.preconfigured_mcp(),
|
||||||
"command": "npx",
|
|
||||||
"args": ["-y", "vibe-kanban", "--mcp"],
|
|
||||||
}),
|
|
||||||
false,
|
false,
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,21 +2,26 @@
|
|||||||
//!
|
//!
|
||||||
//! These helpers abstract over JSON vs TOML formats used by different agents.
|
//! These helpers abstract over JSON vs TOML formats used by different agents.
|
||||||
|
|
||||||
use std::collections::HashMap;
|
use std::{collections::HashMap, sync::LazyLock};
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_json::Value;
|
use serde_json::{Map, Value};
|
||||||
use tokio::fs;
|
use tokio::fs;
|
||||||
use ts_rs::TS;
|
use ts_rs::TS;
|
||||||
|
|
||||||
use crate::executors::ExecutorError;
|
use crate::executors::{CodingAgent, ExecutorError};
|
||||||
|
|
||||||
|
static DEFAULT_MCP_JSON: &str = include_str!("../default_mcp.json");
|
||||||
|
pub static PRECONFIGURED_MCP_SERVERS: LazyLock<Value> = LazyLock::new(|| {
|
||||||
|
serde_json::from_str::<Value>(DEFAULT_MCP_JSON).expect("Failed to parse default MCP JSON")
|
||||||
|
});
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
|
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
|
||||||
pub struct McpConfig {
|
pub struct McpConfig {
|
||||||
servers: HashMap<String, serde_json::Value>,
|
servers: HashMap<String, serde_json::Value>,
|
||||||
pub servers_path: Vec<String>,
|
pub servers_path: Vec<String>,
|
||||||
pub template: serde_json::Value,
|
pub template: serde_json::Value,
|
||||||
pub vibe_kanban: serde_json::Value,
|
pub preconfigured: serde_json::Value,
|
||||||
pub is_toml_config: bool,
|
pub is_toml_config: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -24,14 +29,14 @@ impl McpConfig {
|
|||||||
pub fn new(
|
pub fn new(
|
||||||
servers_path: Vec<String>,
|
servers_path: Vec<String>,
|
||||||
template: serde_json::Value,
|
template: serde_json::Value,
|
||||||
vibe_kanban: serde_json::Value,
|
preconfigured: serde_json::Value,
|
||||||
is_toml_config: bool,
|
is_toml_config: bool,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
servers: HashMap::new(),
|
servers: HashMap::new(),
|
||||||
servers_path,
|
servers_path,
|
||||||
template,
|
template,
|
||||||
vibe_kanban,
|
preconfigured,
|
||||||
is_toml_config,
|
is_toml_config,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -79,3 +84,207 @@ pub async fn write_agent_config(
|
|||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ServerMap = Map<String, Value>;
|
||||||
|
|
||||||
|
fn is_http_server(s: &Map<String, Value>) -> bool {
|
||||||
|
matches!(s.get("type").and_then(Value::as_str), Some("http"))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_stdio(s: &Map<String, Value>) -> bool {
|
||||||
|
!is_http_server(s) && s.get("command").is_some()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extract_meta(mut obj: ServerMap) -> (ServerMap, Option<Value>) {
|
||||||
|
let meta = obj.remove("meta");
|
||||||
|
(obj, meta)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn attach_meta(mut obj: ServerMap, meta: Option<Value>) -> Value {
|
||||||
|
if let Some(m) = meta {
|
||||||
|
obj.insert("meta".to_string(), m);
|
||||||
|
}
|
||||||
|
Value::Object(obj)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ensure_header(headers: &mut Map<String, Value>, key: &str, val: &str) {
|
||||||
|
match headers.get_mut(key) {
|
||||||
|
Some(Value::String(_)) => {}
|
||||||
|
_ => {
|
||||||
|
headers.insert(key.to_string(), Value::String(val.to_string()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn transform_http_servers<F>(mut servers: ServerMap, mut f: F) -> ServerMap
|
||||||
|
where
|
||||||
|
F: FnMut(Map<String, Value>) -> Map<String, Value>,
|
||||||
|
{
|
||||||
|
for (_k, v) in servers.iter_mut() {
|
||||||
|
if let Value::Object(s) = v
|
||||||
|
&& is_http_server(s)
|
||||||
|
{
|
||||||
|
let taken = std::mem::take(s);
|
||||||
|
*s = f(taken);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
servers
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Adapters ---------------------------------------------------------------
|
||||||
|
|
||||||
|
fn adapt_passthrough(servers: ServerMap, meta: Option<Value>) -> Value {
|
||||||
|
attach_meta(servers, meta)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn adapt_gemini(servers: ServerMap, meta: Option<Value>) -> Value {
|
||||||
|
let servers = transform_http_servers(servers, |mut s| {
|
||||||
|
let url = s
|
||||||
|
.remove("url")
|
||||||
|
.unwrap_or_else(|| Value::String(String::new()));
|
||||||
|
let mut headers = s
|
||||||
|
.remove("headers")
|
||||||
|
.and_then(|v| v.as_object().cloned())
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
ensure_header(
|
||||||
|
&mut headers,
|
||||||
|
"Accept",
|
||||||
|
"application/json, text/event-stream",
|
||||||
|
);
|
||||||
|
Map::from_iter([
|
||||||
|
("httpUrl".to_string(), url),
|
||||||
|
("headers".to_string(), Value::Object(headers)),
|
||||||
|
])
|
||||||
|
});
|
||||||
|
attach_meta(servers, meta)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn adapt_cursor(servers: ServerMap, meta: Option<Value>) -> Value {
|
||||||
|
let servers = transform_http_servers(servers, |mut s| {
|
||||||
|
let url = s
|
||||||
|
.remove("url")
|
||||||
|
.unwrap_or_else(|| Value::String(String::new()));
|
||||||
|
let headers = s
|
||||||
|
.remove("headers")
|
||||||
|
.unwrap_or_else(|| Value::Object(Default::default()));
|
||||||
|
Map::from_iter([("url".to_string(), url), ("headers".to_string(), headers)])
|
||||||
|
});
|
||||||
|
attach_meta(servers, meta)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn adapt_codex(mut servers: ServerMap, mut meta: Option<Value>) -> Value {
|
||||||
|
servers.retain(|_, v| v.as_object().map(is_stdio).unwrap_or(false));
|
||||||
|
|
||||||
|
if let Some(Value::Object(ref mut m)) = meta {
|
||||||
|
m.retain(|k, _| servers.contains_key(k));
|
||||||
|
servers.insert("meta".to_string(), Value::Object(std::mem::take(m)));
|
||||||
|
meta = None; // already attached above
|
||||||
|
}
|
||||||
|
attach_meta(servers, meta)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn adapt_opencode(servers: ServerMap, meta: Option<Value>) -> Value {
|
||||||
|
let mut servers = transform_http_servers(servers, |mut s| {
|
||||||
|
let url = s
|
||||||
|
.remove("url")
|
||||||
|
.unwrap_or_else(|| Value::String(String::new()));
|
||||||
|
|
||||||
|
let mut headers = s
|
||||||
|
.remove("headers")
|
||||||
|
.and_then(|v| v.as_object().cloned())
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
ensure_header(
|
||||||
|
&mut headers,
|
||||||
|
"Accept",
|
||||||
|
"application/json, text/event-stream",
|
||||||
|
);
|
||||||
|
|
||||||
|
Map::from_iter([
|
||||||
|
("type".to_string(), Value::String("remote".to_string())),
|
||||||
|
("url".to_string(), url),
|
||||||
|
("headers".to_string(), Value::Object(headers)),
|
||||||
|
("enabled".to_string(), Value::Bool(true)),
|
||||||
|
])
|
||||||
|
});
|
||||||
|
|
||||||
|
for (_k, v) in servers.iter_mut() {
|
||||||
|
if let Value::Object(s) = v
|
||||||
|
&& is_stdio(s)
|
||||||
|
{
|
||||||
|
let command_str = s
|
||||||
|
.remove("command")
|
||||||
|
.and_then(|v| match v {
|
||||||
|
Value::String(s) => Some(s),
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
let mut cmd_vec: Vec<Value> = Vec::new();
|
||||||
|
if !command_str.is_empty() {
|
||||||
|
cmd_vec.push(Value::String(command_str));
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(arr) = s.remove("args").and_then(|v| match v {
|
||||||
|
Value::Array(arr) => Some(arr),
|
||||||
|
_ => None,
|
||||||
|
}) {
|
||||||
|
for a in arr {
|
||||||
|
match a {
|
||||||
|
Value::String(s) => cmd_vec.push(Value::String(s)),
|
||||||
|
other => cmd_vec.push(other), // fall back to raw value if not string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut new_map = Map::new();
|
||||||
|
new_map.insert("type".to_string(), Value::String("local".to_string()));
|
||||||
|
new_map.insert("command".to_string(), Value::Array(cmd_vec));
|
||||||
|
new_map.insert("enabled".to_string(), Value::Bool(true));
|
||||||
|
*s = new_map;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
attach_meta(servers, meta)
|
||||||
|
}
|
||||||
|
|
||||||
|
enum Adapter {
|
||||||
|
Passthrough,
|
||||||
|
Gemini,
|
||||||
|
Cursor,
|
||||||
|
Codex,
|
||||||
|
Opencode,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply_adapter(adapter: Adapter, canonical: Value) -> Value {
|
||||||
|
let (servers_only, meta) = match canonical.as_object() {
|
||||||
|
Some(map) => extract_meta(map.clone()),
|
||||||
|
None => (ServerMap::new(), None),
|
||||||
|
};
|
||||||
|
|
||||||
|
match adapter {
|
||||||
|
Adapter::Passthrough => adapt_passthrough(servers_only, meta),
|
||||||
|
Adapter::Gemini => adapt_gemini(servers_only, meta),
|
||||||
|
Adapter::Cursor => adapt_cursor(servers_only, meta),
|
||||||
|
Adapter::Codex => adapt_codex(servers_only, meta),
|
||||||
|
Adapter::Opencode => adapt_opencode(servers_only, meta),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CodingAgent {
|
||||||
|
pub fn preconfigured_mcp(&self) -> Value {
|
||||||
|
use Adapter::*;
|
||||||
|
|
||||||
|
let adapter = match self {
|
||||||
|
CodingAgent::ClaudeCode(_) | CodingAgent::Amp(_) => Passthrough,
|
||||||
|
CodingAgent::QwenCode(_) | CodingAgent::Gemini(_) => Gemini,
|
||||||
|
CodingAgent::Cursor(_) => Cursor,
|
||||||
|
CodingAgent::Codex(_) => Codex,
|
||||||
|
CodingAgent::Opencode(_) => Opencode,
|
||||||
|
};
|
||||||
|
|
||||||
|
let canonical = PRECONFIGURED_MCP_SERVERS.clone();
|
||||||
|
apply_adapter(adapter, canonical)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
62
frontend/package-lock.json
generated
62
frontend/package-lock.json
generated
@@ -20,6 +20,7 @@
|
|||||||
"@radix-ui/react-dropdown-menu": "^2.1.15",
|
"@radix-ui/react-dropdown-menu": "^2.1.15",
|
||||||
"@radix-ui/react-label": "^2.1.7",
|
"@radix-ui/react-label": "^2.1.7",
|
||||||
"@radix-ui/react-portal": "^1.1.9",
|
"@radix-ui/react-portal": "^1.1.9",
|
||||||
|
"@radix-ui/react-scroll-area": "^1.2.10",
|
||||||
"@radix-ui/react-select": "^2.2.5",
|
"@radix-ui/react-select": "^2.2.5",
|
||||||
"@radix-ui/react-separator": "^1.1.7",
|
"@radix-ui/react-separator": "^1.1.7",
|
||||||
"@radix-ui/react-slot": "^1.2.3",
|
"@radix-ui/react-slot": "^1.2.3",
|
||||||
@@ -1964,6 +1965,67 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@radix-ui/react-scroll-area": {
|
||||||
|
"version": "1.2.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-scroll-area/-/react-scroll-area-1.2.10.tgz",
|
||||||
|
"integrity": "sha512-tAXIa1g3sM5CGpVT0uIbUx/U3Gs5N8T52IICuCtObaos1S8fzsrPXG5WObkQN3S6NVl6wKgPhAIiBGbWnvc97A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/number": "1.1.1",
|
||||||
|
"@radix-ui/primitive": "1.1.3",
|
||||||
|
"@radix-ui/react-compose-refs": "1.1.2",
|
||||||
|
"@radix-ui/react-context": "1.1.2",
|
||||||
|
"@radix-ui/react-direction": "1.1.1",
|
||||||
|
"@radix-ui/react-presence": "1.1.5",
|
||||||
|
"@radix-ui/react-primitive": "2.1.3",
|
||||||
|
"@radix-ui/react-use-callback-ref": "1.1.1",
|
||||||
|
"@radix-ui/react-use-layout-effect": "1.1.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-scroll-area/node_modules/@radix-ui/primitive": {
|
||||||
|
"version": "1.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz",
|
||||||
|
"integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-scroll-area/node_modules/@radix-ui/react-presence": {
|
||||||
|
"version": "1.1.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz",
|
||||||
|
"integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-compose-refs": "1.1.2",
|
||||||
|
"@radix-ui/react-use-layout-effect": "1.1.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@radix-ui/react-select": {
|
"node_modules/@radix-ui/react-select": {
|
||||||
"version": "2.2.5",
|
"version": "2.2.5",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.2.5.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.2.5.tgz",
|
||||||
|
|||||||
@@ -43,6 +43,7 @@
|
|||||||
"click-to-react-component": "^1.1.2",
|
"click-to-react-component": "^1.1.2",
|
||||||
"clsx": "^2.0.0",
|
"clsx": "^2.0.0",
|
||||||
"diff": "^8.0.2",
|
"diff": "^8.0.2",
|
||||||
|
"embla-carousel-react": "^8.6.0",
|
||||||
"fancy-ansi": "^0.1.3",
|
"fancy-ansi": "^0.1.3",
|
||||||
"lucide-react": "^0.539.0",
|
"lucide-react": "^0.539.0",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
|
|||||||
BIN
frontend/public/mcp/context7logo.png
Normal file
BIN
frontend/public/mcp/context7logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 9.2 KiB |
12
frontend/public/mcp/playwright_logo_icon.svg
Normal file
12
frontend/public/mcp/playwright_logo_icon.svg
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" id="Playwright--Streamline-Svg-Logos" height="24" width="24">
|
||||||
|
<desc>
|
||||||
|
Playwright Streamline Icon: https://streamlinehq.com
|
||||||
|
</desc>
|
||||||
|
<path fill="#2d4552" d="M7.99585 13.141725c-0.87725 0.248975 -1.452775 0.685475 -1.8319 1.12165 0.363125 -0.317775 0.849525 -0.609425 1.505675 -0.795425 0.6711 -0.1902 1.243625 -0.188825 1.7167 -0.09755v-0.369925c-0.40355 -0.0369 -0.866225 -0.0075 -1.390475 0.14125Zm-1.872 -3.109775 -3.25795 0.858325s0.059375 0.083875 0.1693 0.195775l2.76235 -0.727875s-0.03915 0.5044 -0.379075 0.9556c0.643 -0.486475 0.705375 -1.281825 0.705375 -1.281825Zm2.727125 7.65675C4.26615 18.923575 1.8404825 13.61025 1.1060875 10.852425c-0.3393 -1.273 -0.487415 -2.2371 -0.5268925 -2.859275 -0.0042425 -0.0646 -0.0022825 -0.11905 0.002285 -0.16895 -0.237835 0.01435 -0.3517015 0.137975 -0.328535 0.49525 0.0394775 0.621825 0.187595 1.585875 0.526895 2.859275C1.5139075 13.936125 3.9399 19.24945 8.52475 18.0146c0.99795 -0.26885 1.747675 -0.758525 2.310475 -1.383625 -0.51875 0.468525 -1.168 0.8375 -1.98425 1.057725Zm0.861575 -10.90855v0.3263h1.79835c-0.0369 -0.115525 -0.074075 -0.219625 -0.110975 -0.3263h-1.687375Z" stroke-width="0.25"></path>
|
||||||
|
<path fill="#2d4552" d="M11.9129 9.46735c0.80875 0.229675 1.2365 0.7967 1.462575 1.2985l0.90175 0.2561s-0.123 -1.756175 -1.711525 -2.2074c-1.486075 -0.422225 -2.400575 0.8257 -2.5118 0.9872 0.4323 -0.308 1.063575 -0.56015 1.859 -0.3344Zm7.178175 1.3066c-1.487425 -0.424125 -2.401575 0.8264 -2.511175 0.985625 0.432625 -0.307625 1.063575 -0.559875 1.85865 -0.3331 0.80745 0.23005 1.23485 0.796375 1.461625 1.298525l0.90305 0.25705s-0.125 -1.756525 -1.71215 -2.2081Zm-0.8959 4.6305 -7.501475 -2.097125s0.0812 0.411725 0.3928 0.94485l6.3159 1.765675c0.519975 -0.30085 0.792775 -0.6134 0.792775 -0.6134ZM12.994375 19.918475C7.054675 18.326 7.77275 10.758025 8.733875 7.171825c0.395725 -1.4779 0.802575 -2.576375 1.13995 -3.312725 -0.2013 -0.041425 -0.368025 0.0646 -0.532775 0.39965 -0.358225 0.726575 -0.8163 1.90955 -1.259625 3.5656 -0.96085 3.586125 -1.67895 11.15385 4.2605 12.746325 2.79955 0.75 4.980475 -0.3899 6.60625 -2.18005 -1.543175 1.3977 -3.513425 2.181325 -5.9538 1.52785Z" stroke-width="0.25"></path>
|
||||||
|
<path fill="#e2574c" d="M9.7126 15.915175V14.388l-4.243175 1.2032s0.313525 -1.82175 2.526475 -2.4495c0.6711 -0.1902 1.2437 -0.1889 1.7167 -0.09755V6.780125h2.124575c-0.231325 -0.714825 -0.4551 -1.26515 -0.64305 -1.64755 -0.310925 -0.632925 -0.62965 -0.21335 -1.35325 0.39185 -0.50965 0.425775 -1.797675 1.33405 -3.7359 1.85635 -1.938275 0.522625 -3.50525 0.384025 -4.15906 0.2708 -0.9268825 -0.1599 -1.4116925 -0.36345 -1.3663375 0.34155 0.03947 0.621825 0.187595 1.58595 0.5268925 2.859275C1.8405325 13.609875 4.266525 18.9232 8.85135 17.68835c1.197625 -0.3227 2.04295 -0.960525 2.6289 -1.7735h-1.76765v0.000325ZM2.865625 10.89025l3.258275 -0.858325s-0.094975 1.25345 -1.31645 1.57545c-1.2218 0.321675 -1.941825 -0.717125 -1.941825 -0.717125Z" stroke-width="0.25"></path>
|
||||||
|
<path fill="#2ead33" d="M21.975075 6.8525c-0.84695 0.148475 -2.878875 0.33345 -5.389975 -0.339625 -2.5118 -0.672675 -4.17835 -1.849175 -4.838625 -2.402175 -0.936 -0.783975 -1.347725 -1.328825 -1.752925 -0.5047 -0.358225 0.726875 -0.816325 1.909875 -1.259725 3.565925 -0.960775 3.586125 -1.67885 11.15385 4.260525 12.7463 5.938125 1.591125 9.09945 -5.322175 10.0603 -8.908625 0.4434 -1.655725 0.637825 -2.9095 0.691325 -3.717925 0.061 -0.915775 -0.568025 -0.64995 -1.7709 -0.439175ZM10.0418 9.81945s0.936 -1.45575 2.523525 -1.00455c1.588525 0.451225 1.711525 2.207425 1.711525 2.207425l-4.23505 -1.202875ZM13.917 16.352c-2.79235 -0.817975 -3.223 -3.04465 -3.223 -3.04465l7.501125 2.0972c0 -0.00035 -1.5141 1.755175 -4.278125 0.94745Zm2.6521 -4.57605s0.9347 -1.45475 2.521925 -1.00225c1.587175 0.4519 1.71215 2.2081 1.71215 2.2081l-4.234075 -1.20585Z" stroke-width="0.25"></path>
|
||||||
|
<path fill="#d65348" d="M8.2299 14.808525 5.4695 15.590875s0.29985 -1.708225 2.33335 -2.385175l-1.563075 -5.865975 -0.135075 0.04105c-1.93825 0.5227 -3.505225 0.384025 -4.15903 0.2708 -0.9268775 -0.159825 -1.411685 -0.36345 -1.3663375 0.341625 0.0394775 0.621825 0.187595 1.585875 0.5268925 2.85925 0.7340675 2.757425 3.160075 8.07075 7.744875 6.8359l0.135075 -0.042425 -0.756275 -2.8374ZM2.8657 10.8903l3.258275 -0.858375s-0.094975 1.25345 -1.316425 1.57545c-1.221825 0.321675 -1.94185 -0.717075 -1.94185 -0.717075Z" stroke-width="0.25"></path>
|
||||||
|
<path fill="#1d8d22" d="m14.04295 16.382625 -0.1263 -0.0307c-2.792325 -0.8179 -3.223 -3.044575 -3.223 -3.044575l3.86805 1.0812 2.047825 -7.86915 -0.024775 -0.006525c-2.5118 -0.672675 -4.17825 -1.849175 -4.838625 -2.402175 -0.936 -0.783975 -1.347725 -1.328825 -1.752925 -0.5047 -0.357875 0.726875 -0.815975 1.909875 -1.259375 3.565925 -0.960775 3.586125 -1.67885 11.15385 4.260525 12.74625l0.121725 0.027425 0.926875 -3.562975ZM10.0418 9.819475s0.936 -1.455775 2.523525 -1.004575c1.588525 0.451225 1.711525 2.207425 1.711525 2.207425l-4.23505 -1.20285Z" stroke-width="0.25"></path>
|
||||||
|
<path fill="#c04b41" d="m8.37055 14.7683 -0.740275 0.2101c0.174875 0.9859 0.483125 1.93205 0.966975 2.7679 0.0842 -0.0186 0.167725 -0.034575 0.2535 -0.058075 0.2248 -0.06065 0.43325 -0.13575 0.63395 -0.21765 -0.5406 -0.802225 -0.898225 -1.726175 -1.11415 -2.702275Zm-0.289075 -6.9439c-0.3804 1.419825 -0.720725 3.46345 -0.62705 5.51325 0.167675 -0.072775 0.3448 -0.140575 0.54155 -0.1964l0.13705 -0.030625c-0.167075 -2.189525 0.194075 -4.4207 0.600925 -5.93875 0.103125 -0.384025 0.206525 -0.741225 0.3096 -1.07435 -0.166025 0.105675 -0.3448 0.213975 -0.548425 0.32555 -0.137325 0.42385 -0.276 0.887125 -0.41365 1.401325Z" stroke-width="0.25"></path>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 5.6 KiB |
260
frontend/src/components/ui/carousel.tsx
Normal file
260
frontend/src/components/ui/carousel.tsx
Normal file
@@ -0,0 +1,260 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import useEmblaCarousel, {
|
||||||
|
type UseEmblaCarouselType,
|
||||||
|
} from 'embla-carousel-react';
|
||||||
|
import { ArrowLeft, ArrowRight } from 'lucide-react';
|
||||||
|
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
|
||||||
|
type CarouselApi = UseEmblaCarouselType[1];
|
||||||
|
type UseCarouselParameters = Parameters<typeof useEmblaCarousel>;
|
||||||
|
type CarouselOptions = UseCarouselParameters[0];
|
||||||
|
type CarouselPlugin = UseCarouselParameters[1];
|
||||||
|
|
||||||
|
type CarouselProps = {
|
||||||
|
opts?: CarouselOptions;
|
||||||
|
plugins?: CarouselPlugin;
|
||||||
|
orientation?: 'horizontal' | 'vertical';
|
||||||
|
setApi?: (api: CarouselApi) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
type CarouselContextProps = {
|
||||||
|
carouselRef: ReturnType<typeof useEmblaCarousel>[0];
|
||||||
|
api: ReturnType<typeof useEmblaCarousel>[1];
|
||||||
|
scrollPrev: () => void;
|
||||||
|
scrollNext: () => void;
|
||||||
|
canScrollPrev: boolean;
|
||||||
|
canScrollNext: boolean;
|
||||||
|
} & CarouselProps;
|
||||||
|
|
||||||
|
const CarouselContext = React.createContext<CarouselContextProps | null>(null);
|
||||||
|
|
||||||
|
function useCarousel() {
|
||||||
|
const context = React.useContext(CarouselContext);
|
||||||
|
|
||||||
|
if (!context) {
|
||||||
|
throw new Error('useCarousel must be used within a <Carousel />');
|
||||||
|
}
|
||||||
|
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Carousel = React.forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
React.HTMLAttributes<HTMLDivElement> & CarouselProps
|
||||||
|
>(
|
||||||
|
(
|
||||||
|
{
|
||||||
|
orientation = 'horizontal',
|
||||||
|
opts,
|
||||||
|
setApi,
|
||||||
|
plugins,
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
},
|
||||||
|
ref
|
||||||
|
) => {
|
||||||
|
const [carouselRef, api] = useEmblaCarousel(
|
||||||
|
{
|
||||||
|
...opts,
|
||||||
|
axis: orientation === 'horizontal' ? 'x' : 'y',
|
||||||
|
},
|
||||||
|
plugins
|
||||||
|
);
|
||||||
|
const [canScrollPrev, setCanScrollPrev] = React.useState(false);
|
||||||
|
const [canScrollNext, setCanScrollNext] = React.useState(false);
|
||||||
|
|
||||||
|
const onSelect = React.useCallback((api: CarouselApi) => {
|
||||||
|
if (!api) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setCanScrollPrev(api.canScrollPrev());
|
||||||
|
setCanScrollNext(api.canScrollNext());
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const scrollPrev = React.useCallback(() => {
|
||||||
|
api?.scrollPrev();
|
||||||
|
}, [api]);
|
||||||
|
|
||||||
|
const scrollNext = React.useCallback(() => {
|
||||||
|
api?.scrollNext();
|
||||||
|
}, [api]);
|
||||||
|
|
||||||
|
const handleKeyDown = React.useCallback(
|
||||||
|
(event: React.KeyboardEvent<HTMLDivElement>) => {
|
||||||
|
if (event.key === 'ArrowLeft') {
|
||||||
|
event.preventDefault();
|
||||||
|
scrollPrev();
|
||||||
|
} else if (event.key === 'ArrowRight') {
|
||||||
|
event.preventDefault();
|
||||||
|
scrollNext();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[scrollPrev, scrollNext]
|
||||||
|
);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (!api || !setApi) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setApi(api);
|
||||||
|
}, [api, setApi]);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (!api) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
onSelect(api);
|
||||||
|
api.on('reInit', onSelect);
|
||||||
|
api.on('select', onSelect);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
api?.off('select', onSelect);
|
||||||
|
};
|
||||||
|
}, [api, onSelect]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CarouselContext.Provider
|
||||||
|
value={{
|
||||||
|
carouselRef,
|
||||||
|
api: api,
|
||||||
|
opts,
|
||||||
|
orientation:
|
||||||
|
orientation || (opts?.axis === 'y' ? 'vertical' : 'horizontal'),
|
||||||
|
scrollPrev,
|
||||||
|
scrollNext,
|
||||||
|
canScrollPrev,
|
||||||
|
canScrollNext,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
onKeyDownCapture={handleKeyDown}
|
||||||
|
className={cn('relative', className)}
|
||||||
|
role="region"
|
||||||
|
aria-roledescription="carousel"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</CarouselContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
Carousel.displayName = 'Carousel';
|
||||||
|
|
||||||
|
const CarouselContent = React.forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
React.HTMLAttributes<HTMLDivElement>
|
||||||
|
>(({ className, ...props }, ref) => {
|
||||||
|
const { carouselRef, orientation } = useCarousel();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={carouselRef} className="overflow-hidden">
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
'flex',
|
||||||
|
orientation === 'horizontal' ? '-ml-4' : '-mt-4 flex-col',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
CarouselContent.displayName = 'CarouselContent';
|
||||||
|
|
||||||
|
const CarouselItem = React.forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
React.HTMLAttributes<HTMLDivElement>
|
||||||
|
>(({ className, ...props }, ref) => {
|
||||||
|
const { orientation } = useCarousel();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
role="group"
|
||||||
|
aria-roledescription="slide"
|
||||||
|
className={cn(
|
||||||
|
'min-w-0 shrink-0 grow-0 basis-full',
|
||||||
|
orientation === 'horizontal' ? 'pl-4' : 'pt-4',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
CarouselItem.displayName = 'CarouselItem';
|
||||||
|
|
||||||
|
const CarouselPrevious = React.forwardRef<
|
||||||
|
HTMLButtonElement,
|
||||||
|
React.ComponentProps<typeof Button>
|
||||||
|
>(({ className, variant = 'outline', size = 'icon', ...props }, ref) => {
|
||||||
|
const { orientation, scrollPrev, canScrollPrev } = useCarousel();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
ref={ref}
|
||||||
|
variant={variant}
|
||||||
|
size={size}
|
||||||
|
className={cn(
|
||||||
|
'absolute h-8 w-8 rounded-full',
|
||||||
|
orientation === 'horizontal'
|
||||||
|
? '-left-12 top-1/2 -translate-y-1/2'
|
||||||
|
: '-top-12 left-1/2 -translate-x-1/2 rotate-90',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
disabled={!canScrollPrev}
|
||||||
|
onClick={scrollPrev}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<ArrowLeft className="h-4 w-4" />
|
||||||
|
<span className="sr-only">Previous slide</span>
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
CarouselPrevious.displayName = 'CarouselPrevious';
|
||||||
|
|
||||||
|
const CarouselNext = React.forwardRef<
|
||||||
|
HTMLButtonElement,
|
||||||
|
React.ComponentProps<typeof Button>
|
||||||
|
>(({ className, variant = 'outline', size = 'icon', ...props }, ref) => {
|
||||||
|
const { orientation, scrollNext, canScrollNext } = useCarousel();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
ref={ref}
|
||||||
|
variant={variant}
|
||||||
|
size={size}
|
||||||
|
className={cn(
|
||||||
|
'absolute h-8 w-8 rounded-full',
|
||||||
|
orientation === 'horizontal'
|
||||||
|
? '-right-12 top-1/2 -translate-y-1/2'
|
||||||
|
: '-bottom-12 left-1/2 -translate-x-1/2 rotate-90',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
disabled={!canScrollNext}
|
||||||
|
onClick={scrollNext}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<ArrowRight className="h-4 w-4" />
|
||||||
|
<span className="sr-only">Next slide</span>
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
CarouselNext.displayName = 'CarouselNext';
|
||||||
|
|
||||||
|
export {
|
||||||
|
type CarouselApi,
|
||||||
|
Carousel,
|
||||||
|
CarouselContent,
|
||||||
|
CarouselItem,
|
||||||
|
CarouselPrevious,
|
||||||
|
CarouselNext,
|
||||||
|
};
|
||||||
@@ -53,32 +53,31 @@ export class McpConfigStrategyGeneral {
|
|||||||
return current;
|
return current;
|
||||||
}
|
}
|
||||||
|
|
||||||
static addVibeKanbanToConfig(
|
static addPreconfiguredToConfig(
|
||||||
mcp_config: McpConfig,
|
mcp_config: McpConfig,
|
||||||
existingConfig: Record<string, any>
|
existingConfig: Record<string, any>,
|
||||||
|
serverKey: string
|
||||||
): Record<string, any> {
|
): Record<string, any> {
|
||||||
// Clone the existing config to avoid mutations
|
const preconf = mcp_config.preconfigured as Record<string, any>;
|
||||||
const updatedConfig = JSON.parse(JSON.stringify(existingConfig));
|
if (!preconf || typeof preconf !== 'object' || !(serverKey in preconf)) {
|
||||||
let current = updatedConfig;
|
throw new Error(`Unknown preconfigured server '${serverKey}'`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const updated = JSON.parse(JSON.stringify(existingConfig || {}));
|
||||||
|
let current = updated;
|
||||||
|
|
||||||
// Navigate to the correct location for servers (all except the last element)
|
|
||||||
for (let i = 0; i < mcp_config.servers_path.length - 1; i++) {
|
for (let i = 0; i < mcp_config.servers_path.length - 1; i++) {
|
||||||
const key = mcp_config.servers_path[i];
|
const key = mcp_config.servers_path[i];
|
||||||
if (!current[key]) {
|
if (!current[key] || typeof current[key] !== 'object') current[key] = {};
|
||||||
current[key] = {};
|
|
||||||
}
|
|
||||||
current = current[key];
|
current = current[key];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get or create the servers object at the final path element
|
|
||||||
const lastKey = mcp_config.servers_path[mcp_config.servers_path.length - 1];
|
const lastKey = mcp_config.servers_path[mcp_config.servers_path.length - 1];
|
||||||
if (!current[lastKey]) {
|
if (!current[lastKey] || typeof current[lastKey] !== 'object')
|
||||||
current[lastKey] = {};
|
current[lastKey] = {};
|
||||||
}
|
|
||||||
|
|
||||||
// Add vibe_kanban server with the config from the schema
|
current[lastKey][serverKey] = preconf[serverKey];
|
||||||
current[lastKey]['vibe_kanban'] = mcp_config.vibe_kanban;
|
|
||||||
|
|
||||||
return updatedConfig;
|
return updated;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,13 @@ import {
|
|||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from '@/components/ui/select';
|
} from '@/components/ui/select';
|
||||||
|
import {
|
||||||
|
Carousel,
|
||||||
|
CarouselContent,
|
||||||
|
CarouselItem,
|
||||||
|
CarouselNext,
|
||||||
|
CarouselPrevious,
|
||||||
|
} from '@/components/ui/carousel';
|
||||||
import { Label } from '@/components/ui/label';
|
import { Label } from '@/components/ui/label';
|
||||||
import { Alert, AlertDescription } from '@/components/ui/alert';
|
import { Alert, AlertDescription } from '@/components/ui/alert';
|
||||||
import { JSONEditor } from '@/components/ui/json-editor';
|
import { JSONEditor } from '@/components/ui/json-editor';
|
||||||
@@ -119,29 +126,6 @@ export function McpSettings() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleConfigureVibeKanban = async () => {
|
|
||||||
if (!selectedProfile || !mcpConfig) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Parse existing configuration
|
|
||||||
const existingConfig = mcpServers.trim() ? JSON.parse(mcpServers) : {};
|
|
||||||
|
|
||||||
// Add vibe_kanban to the existing configuration using the schema
|
|
||||||
const updatedConfig = McpConfigStrategyGeneral.addVibeKanbanToConfig(
|
|
||||||
mcpConfig,
|
|
||||||
existingConfig
|
|
||||||
);
|
|
||||||
|
|
||||||
// Update the textarea with the new configuration
|
|
||||||
const configJson = JSON.stringify(updatedConfig, null, 2);
|
|
||||||
setMcpServers(configJson);
|
|
||||||
setMcpError(null);
|
|
||||||
} catch (err) {
|
|
||||||
setMcpError('Failed to configure vibe-kanban MCP server');
|
|
||||||
console.error('Error configuring vibe-kanban:', err);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleApplyMcpServers = async () => {
|
const handleApplyMcpServers = async () => {
|
||||||
if (!selectedProfile || !mcpConfig) return;
|
if (!selectedProfile || !mcpConfig) return;
|
||||||
|
|
||||||
@@ -200,6 +184,36 @@ export function McpSettings() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const addServer = (key: string) => {
|
||||||
|
try {
|
||||||
|
const existing = mcpServers.trim() ? JSON.parse(mcpServers) : {};
|
||||||
|
const updated = McpConfigStrategyGeneral.addPreconfiguredToConfig(
|
||||||
|
mcpConfig!,
|
||||||
|
existing,
|
||||||
|
key
|
||||||
|
);
|
||||||
|
setMcpServers(JSON.stringify(updated, null, 2));
|
||||||
|
setMcpError(null);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
setMcpError(
|
||||||
|
err instanceof Error
|
||||||
|
? err.message
|
||||||
|
: 'Failed to add preconfigured server'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const preconfigured = (mcpConfig?.preconfigured ?? {}) as Record<string, any>;
|
||||||
|
const meta = (preconfigured.meta ?? {}) as Record<
|
||||||
|
string,
|
||||||
|
{ name?: string; description?: string; url?: string; icon?: string }
|
||||||
|
>;
|
||||||
|
const servers = Object.fromEntries(
|
||||||
|
Object.entries(preconfigured).filter(([k]) => k !== 'meta')
|
||||||
|
) as Record<string, any>;
|
||||||
|
const getMetaFor = (key: string) => meta[key] || {};
|
||||||
|
|
||||||
if (!config) {
|
if (!config) {
|
||||||
return (
|
return (
|
||||||
<div className="py-8">
|
<div className="py-8">
|
||||||
@@ -324,18 +338,83 @@ export function McpSettings() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="pt-4">
|
{mcpConfig?.preconfigured &&
|
||||||
<Button
|
typeof mcpConfig.preconfigured === 'object' && (
|
||||||
onClick={handleConfigureVibeKanban}
|
<div className="pt-4">
|
||||||
disabled={mcpApplying || mcpLoading || !selectedProfile}
|
<Label>Popular servers</Label>
|
||||||
className="w-64"
|
<p className="text-sm text-muted-foreground mb-2">
|
||||||
>
|
Click a card to insert that MCP Server into the JSON
|
||||||
Add Vibe-Kanban MCP
|
above.
|
||||||
</Button>
|
</p>
|
||||||
<p className="text-sm text-muted-foreground mt-2">
|
|
||||||
Automatically adds the Vibe-Kanban MCP server configuration.
|
<div className="relative overflow-hidden rounded-xl border bg-background">
|
||||||
</p>
|
<Carousel className="w-full px-4 py-3">
|
||||||
</div>
|
<CarouselContent className="gap-3 justify-center">
|
||||||
|
{Object.entries(servers).map(([key]) => {
|
||||||
|
const metaObj = getMetaFor(key) as {
|
||||||
|
name?: string;
|
||||||
|
description?: string;
|
||||||
|
url?: string;
|
||||||
|
icon?: string;
|
||||||
|
};
|
||||||
|
const name = metaObj.name || key;
|
||||||
|
const description =
|
||||||
|
metaObj.description || 'No description';
|
||||||
|
const icon = metaObj.icon
|
||||||
|
? `/${metaObj.icon}`
|
||||||
|
: null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CarouselItem
|
||||||
|
key={name}
|
||||||
|
className="sm:basis-1/3 lg:basis-1/4"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => addServer(key)}
|
||||||
|
aria-label={`Add ${name} to config`}
|
||||||
|
className="group w-full text-left outline-none"
|
||||||
|
>
|
||||||
|
<Card className="h-32 rounded-xl border hover:shadow-md transition">
|
||||||
|
<CardHeader className="pb-0">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="w-6 h-6 rounded-lg border bg-muted grid place-items-center overflow-hidden">
|
||||||
|
{icon ? (
|
||||||
|
<img
|
||||||
|
src={icon}
|
||||||
|
alt=""
|
||||||
|
className="w-full h-full object-cover"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<span className="font-semibold">
|
||||||
|
{name.slice(0, 1).toUpperCase()}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<CardTitle className="text-base font-medium truncate">
|
||||||
|
{name}
|
||||||
|
</CardTitle>
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
|
||||||
|
<CardContent className="pt-2 px-4">
|
||||||
|
<p className="text-sm text-muted-foreground line-clamp-3">
|
||||||
|
{description}
|
||||||
|
</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</button>
|
||||||
|
</CarouselItem>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</CarouselContent>
|
||||||
|
|
||||||
|
<CarouselPrevious className="left-2 top-1/2 -translate-y-1/2 h-8 w-8 rounded-full border bg-background/80 shadow-sm backdrop-blur hover:bg-background" />
|
||||||
|
<CarouselNext className="right-2 top-1/2 -translate-y-1/2 h-8 w-8 rounded-full border bg-background/80 shadow-sm backdrop-blur hover:bg-background" />
|
||||||
|
</Carousel>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
"npx-cli/dist/**"
|
"npx-cli/dist/**"
|
||||||
],
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
"format": "cargo fmt --all && cd frontend && npm run format",
|
||||||
"check": "npm run frontend:check && npm run backend:check",
|
"check": "npm run frontend:check && npm run backend:check",
|
||||||
"dev": "export FRONTEND_PORT=$(node scripts/setup-dev-environment.js frontend) && export BACKEND_PORT=$(node scripts/setup-dev-environment.js backend) && concurrently \"npm run backend:dev:watch\" \"npm run frontend:dev\"",
|
"dev": "export FRONTEND_PORT=$(node scripts/setup-dev-environment.js frontend) && export BACKEND_PORT=$(node scripts/setup-dev-environment.js backend) && concurrently \"npm run backend:dev:watch\" \"npm run frontend:dev\"",
|
||||||
"test:npm": "./test-npm-package.sh",
|
"test:npm": "./test-npm-package.sh",
|
||||||
|
|||||||
28
pnpm-lock.yaml
generated
28
pnpm-lock.yaml
generated
@@ -111,6 +111,9 @@ importers:
|
|||||||
diff:
|
diff:
|
||||||
specifier: ^8.0.2
|
specifier: ^8.0.2
|
||||||
version: 8.0.2
|
version: 8.0.2
|
||||||
|
embla-carousel-react:
|
||||||
|
specifier: ^8.6.0
|
||||||
|
version: 8.6.0(react@18.3.1)
|
||||||
fancy-ansi:
|
fancy-ansi:
|
||||||
specifier: ^0.1.3
|
specifier: ^0.1.3
|
||||||
version: 0.1.3
|
version: 0.1.3
|
||||||
@@ -2000,6 +2003,19 @@ packages:
|
|||||||
electron-to-chromium@1.5.170:
|
electron-to-chromium@1.5.170:
|
||||||
resolution: {integrity: sha512-GP+M7aeluQo9uAyiTCxgIj/j+PrWhMlY7LFVj8prlsPljd0Fdg9AprlfUi+OCSFWy9Y5/2D/Jrj9HS8Z4rpKWA==}
|
resolution: {integrity: sha512-GP+M7aeluQo9uAyiTCxgIj/j+PrWhMlY7LFVj8prlsPljd0Fdg9AprlfUi+OCSFWy9Y5/2D/Jrj9HS8Z4rpKWA==}
|
||||||
|
|
||||||
|
embla-carousel-react@8.6.0:
|
||||||
|
resolution: {integrity: sha512-0/PjqU7geVmo6F734pmPqpyHqiM99olvyecY7zdweCw+6tKEXnrE90pBiBbMMU8s5tICemzpQ3hi5EpxzGW+JA==}
|
||||||
|
peerDependencies:
|
||||||
|
react: ^16.8.0 || ^17.0.1 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc
|
||||||
|
|
||||||
|
embla-carousel-reactive-utils@8.6.0:
|
||||||
|
resolution: {integrity: sha512-fMVUDUEx0/uIEDM0Mz3dHznDhfX+znCCDCeIophYb1QGVM7YThSWX+wz11zlYwWFOr74b4QLGg0hrGPJeG2s4A==}
|
||||||
|
peerDependencies:
|
||||||
|
embla-carousel: 8.6.0
|
||||||
|
|
||||||
|
embla-carousel@8.6.0:
|
||||||
|
resolution: {integrity: sha512-SjWyZBHJPbqxHOzckOfo8lHisEaJWmwd23XppYFYVh10bU66/Pn5tkVkbkCMZVdbUE5eTCI2nD8OyIP4Z+uwkA==}
|
||||||
|
|
||||||
emoji-regex@8.0.0:
|
emoji-regex@8.0.0:
|
||||||
resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
|
resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
|
||||||
|
|
||||||
@@ -5179,6 +5195,18 @@ snapshots:
|
|||||||
|
|
||||||
electron-to-chromium@1.5.170: {}
|
electron-to-chromium@1.5.170: {}
|
||||||
|
|
||||||
|
embla-carousel-react@8.6.0(react@18.3.1):
|
||||||
|
dependencies:
|
||||||
|
embla-carousel: 8.6.0
|
||||||
|
embla-carousel-reactive-utils: 8.6.0(embla-carousel@8.6.0)
|
||||||
|
react: 18.3.1
|
||||||
|
|
||||||
|
embla-carousel-reactive-utils@8.6.0(embla-carousel@8.6.0):
|
||||||
|
dependencies:
|
||||||
|
embla-carousel: 8.6.0
|
||||||
|
|
||||||
|
embla-carousel@8.6.0: {}
|
||||||
|
|
||||||
emoji-regex@8.0.0: {}
|
emoji-regex@8.0.0: {}
|
||||||
|
|
||||||
emoji-regex@9.2.2: {}
|
emoji-regex@9.2.2: {}
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ export type SearchMode = "taskform" | "settings";
|
|||||||
|
|
||||||
export type ExecutorAction = { typ: ExecutorActionType, next_action: ExecutorAction | null, };
|
export type ExecutorAction = { typ: ExecutorActionType, next_action: ExecutorAction | null, };
|
||||||
|
|
||||||
export type McpConfig = { servers: { [key in string]?: JsonValue }, servers_path: Array<string>, template: JsonValue, vibe_kanban: JsonValue, is_toml_config: boolean, };
|
export type McpConfig = { servers: { [key in string]?: JsonValue }, servers_path: Array<string>, template: JsonValue, preconfigured: JsonValue, is_toml_config: boolean, };
|
||||||
|
|
||||||
export type ExecutorActionType = { "type": "CodingAgentInitialRequest" } & CodingAgentInitialRequest | { "type": "CodingAgentFollowUpRequest" } & CodingAgentFollowUpRequest | { "type": "ScriptRequest" } & ScriptRequest;
|
export type ExecutorActionType = { "type": "CodingAgentInitialRequest" } & CodingAgentInitialRequest | { "type": "CodingAgentFollowUpRequest" } & CodingAgentFollowUpRequest | { "type": "ScriptRequest" } & ScriptRequest;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user