Skip to content
Clone

feat(mcp): serve the MCP API from a standalone service

Add `lib/mcp-api` and `app/mcp`, exposing the GitData tools over MCP
streamable HTTP on port 3007 so external agents can drive issues, pull
requests, repositories and spaces without a browser session.

- `lib/mcp-api` holds the tool catalog: args/issue/pull/repo/space/me
  tools, the MCP server wiring (rmcp), request auth and client-IP
  gating for internal keys, and settings.
- `app/mcp` is the host binary: config, telemetry, healthz and the
  streamable HTTP endpoint under `/v1`.
- Config gains an `[mcp]` section (`lib/config/src/mcp.rs`):
  `allowed_hosts` (empty keeps the rmcp loopback default),
  `internal_cidrs` and `trusted_proxies` for the X-Forwarded-For based
  internal-key check, plus `[[mcp.internal_keys]]` entries that accept
  either `token` or `token_hash` (sha256 hex, recommended).
- The chart ships `templates/mcp.yaml` (Deployment/Service plus an
  optional Ingress and PDB) and an `mcp` values block; prod values set
  the mcp.gitdata.ai host, the cluster CIDRs and the ingress proxy CIDR,
  and keep the internal key in the PROD_SECRET_VALUES secret.
ZhenYi committed
parent f3091ff commit 808e816 Browse files

25 files changed

+7480 −1

File tree 25
Added app/mcp/src/main.rs View app/mcp/src/main.rs
@@ -0,0 +1,73 @@
1use std::net::SocketAddr;
2
3use anyhow::Context;
4use axum::Router;
5use axum::http::StatusCode;
6use config::scoped::{McpHostConfig, ScopedConfig};
7use tokio::net::TcpListener;
8use tower_http::limit::RequestBodyLimitLayer;
9use tower_http::trace::{DefaultMakeSpan, DefaultOnResponse, TraceLayer};
10use tracing::{Level, info};
11
12const MAX_REQUEST_BODY_BYTES: usize = 2 * 1024 * 1024;
13
14#[tokio::main]
15async fn main() -> anyhow::Result<()> {
16 export::telemetry::init_tracing();
17
18 let cfg = McpHostConfig::load().context("loading configuration")?;
19 if !cfg.mcp.enabled() {
20 info!("mcp host is disabled by config; exiting");
21 return Ok(());
22 }
23
24 info!("connecting to database / redis / storage / email");
25 let state = export::AppState::with_config(cfg.clone().into_app_config())
26 .await
27 .context("building AppState")?;
28 info!("dependencies ready");
29
30 let health = export::health::router_with(Some(state.database.clone()), state.redis.clone());
31 let mcp = mcp_api::router(state).await;
32
33 let router: Router = Router::new()
34 .merge(health)
35 .nest("/v1", mcp)
36 .fallback(not_found)
37 .layer(RequestBodyLimitLayer::new(MAX_REQUEST_BODY_BYTES))
38 .layer(
39 TraceLayer::new_for_http()
40 .make_span_with(
41 DefaultMakeSpan::new()
42 .level(Level::INFO)
43 .include_headers(false),
44 )
45 .on_response(DefaultOnResponse::new().level(Level::INFO)),
46 )
47 .layer(export::middleware::AppTraceLayer::new());
48
49 let bind_addr = cfg.mcp.bind_addr().to_string();
50 let port = cfg.mcp.port();
51 let addr: SocketAddr = format!("{bind_addr}:{port}")
52 .parse()
53 .with_context(|| format!("parsing bind address {bind_addr}:{port}"))?;
54 let listener = TcpListener::bind(addr)
55 .await
56 .with_context(|| format!("binding {addr}"))?;
57 info!(%addr, "mcp host listening");
58
59 axum::serve(
60 listener,
61 router.into_make_service_with_connect_info::<SocketAddr>(),
62 )
63 .with_graceful_shutdown(export::telemetry::shutdown_signal())
64 .await
65 .context("axum server error")?;
66
67 info!("mcp host shut down");
68 Ok(())
69}
70
71async fn not_found() -> StatusCode {
72 StatusCode::NOT_FOUND
73}
Added app/mcp/Cargo.toml View app/mcp/Cargo.toml
@@ -0,0 +1,15 @@
1[package]
2name = "mcp"
3version.workspace = true
4edition.workspace = true
5publish = false
6
7[dependencies]
8mcp-api = { workspace = true }
9config = { workspace = true }
10export = { workspace = true }
11axum = { workspace = true }
12tokio = { workspace = true, features = ["signal", "net", "macros", "rt-multi-thread"] }
13tower-http = { workspace = true }
14tracing = { workspace = true }
15anyhow = { workspace = true }
Added chart/templates/mcp.yaml View chart/templates/mcp.yaml
@@ -0,0 +1,157 @@
1{{- if .Values.mcp.enabled }}
2apiVersion: apps/v1
3kind: Deployment
4metadata:
5 name: {{ printf "%s-mcp" (include "gitdata.fullname" .) }}
6 labels:
7 {{- include "gitdata.labels" . | nindent 4 }}
8 app.kubernetes.io/component: mcp
9spec:
10 replicas: {{ .Values.mcp.replicaCount }}
11 revisionHistoryLimit: 3
12 selector:
13 matchLabels:
14 {{- include "gitdata.selectorLabels" . | nindent 6 }}
15 app.kubernetes.io/component: mcp
16 template:
17 metadata:
18 labels:
19 {{- include "gitdata.selectorLabels" . | nindent 8 }}
20 app.kubernetes.io/component: mcp
21 {{- with .Values.podLabels }}
22 {{- toYaml . | nindent 8 }}
23 {{- end }}
24 annotations:
25 checksum/env: {{ toYaml .Values.config.env | sha256sum }}
26 {{- with .Values.podAnnotations }}
27 {{- toYaml . | nindent 8 }}
28 {{- end }}
29 spec:
30 serviceAccountName: {{ include "gitdata.serviceAccountName" . }}
31 {{- include "gitdata.imagePullSecrets" . | nindent 6 }}
32 terminationGracePeriodSeconds: 30
33 securityContext:
34 {{- toYaml .Values.podSecurityContext | nindent 8 }}
35 {{- with .Values.nodeSelector }}
36 nodeSelector:
37 {{- toYaml . | nindent 8 }}
38 {{- end }}
39 {{- with .Values.tolerations }}
40 tolerations:
41 {{- toYaml . | nindent 8 }}
42 {{- end }}
43 containers:
44 - name: mcp
45 image: {{ include "gitdata.image" . }}
46 imagePullPolicy: {{ .Values.image.pullPolicy }}
47 args: ["mcp"]
48 {{- include "gitdata.envFrom" . | nindent 10 }}
49 env:
50 {{- include "gitdata.env" . | nindent 12 }}
51 {{- with .Values.mcp.extraEnv }}
52 {{- toYaml . | nindent 12 }}
53 {{- end }}
54 ports:
55 - name: http
56 containerPort: {{ .Values.mcp.port }}
57 livenessProbe:
58 httpGet: { path: /healthz, port: http }
59 initialDelaySeconds: 10
60 periodSeconds: 15
61 timeoutSeconds: 3
62 readinessProbe:
63 httpGet: { path: /readyz, port: http }
64 initialDelaySeconds: 5
65 periodSeconds: 10
66 timeoutSeconds: 3
67 failureThreshold: 3
68 securityContext:
69 {{- toYaml .Values.securityContext | nindent 12 }}
70 resources:
71 {{- toYaml .Values.mcp.resources | nindent 12 }}
72 volumeMounts:
73 - name: repos
74 mountPath: {{ .Values.persistence.repos.mountPath }}
75 volumes:
76 - name: repos
77 {{- if .Values.persistence.repos.existingClaim }}
78 persistentVolumeClaim:
79 claimName: {{ .Values.persistence.repos.existingClaim }}
80 {{- else if .Values.persistence.repos.enabled }}
81 persistentVolumeClaim:
82 claimName: {{ printf "%s-repos" (include "gitdata.fullname" .) }}
83 {{- else }}
84 emptyDir: {}
85 {{- end }}
86---
87apiVersion: v1
88kind: Service
89metadata:
90 name: {{ printf "%s-mcp" (include "gitdata.fullname" .) }}
91 labels:
92 {{- include "gitdata.labels" . | nindent 4 }}
93 app.kubernetes.io/component: mcp
94spec:
95 type: ClusterIP
96 selector:
97 {{- include "gitdata.selectorLabels" . | nindent 4 }}
98 app.kubernetes.io/component: mcp
99 ports:
100 - name: http
101 port: {{ .Values.mcp.port }}
102 targetPort: http
103{{- if .Values.mcp.ingress.enabled }}
104---
105apiVersion: networking.k8s.io/v1
106kind: Ingress
107metadata:
108 name: {{ printf "%s-mcp" (include "gitdata.fullname" .) }}
109 labels:
110 {{- include "gitdata.labels" . | nindent 4 }}
111 app.kubernetes.io/component: mcp
112 annotations:
113 {{- with .Values.mcp.ingress.annotations }}
114 {{- toYaml . | nindent 4 }}
115 {{- end }}
116 {{- if and .Values.mcp.ingress.tls.enabled .Values.mcp.ingress.tls.clusterIssuer }}
117 cert-manager.io/cluster-issuer: {{ .Values.mcp.ingress.tls.clusterIssuer }}
118 {{- end }}
119spec:
120 {{- with .Values.mcp.ingress.className }}
121 ingressClassName: {{ . }}
122 {{- end }}
123 {{- if .Values.mcp.ingress.tls.enabled }}
124 tls:
125 - hosts:
126 - {{ .Values.mcp.ingress.host | quote }}
127 secretName: {{ default (printf "%s-mcp-tls" (include "gitdata.fullname" .)) .Values.mcp.ingress.tls.secretName }}
128 {{- end }}
129 rules:
130 - host: {{ .Values.mcp.ingress.host | quote }}
131 http:
132 paths:
133 - path: /
134 pathType: Prefix
135 backend:
136 service:
137 name: {{ printf "%s-mcp" (include "gitdata.fullname" .) }}
138 port:
139 name: http
140{{- end }}
141{{- if .Values.mcp.pdb.enabled }}
142---
143apiVersion: policy/v1
144kind: PodDisruptionBudget
145metadata:
146 name: {{ printf "%s-mcp" (include "gitdata.fullname" .) }}
147 labels:
148 {{- include "gitdata.labels" . | nindent 4 }}
149 app.kubernetes.io/component: mcp
150spec:
151 minAvailable: {{ .Values.mcp.pdb.minAvailable }}
152 selector:
153 matchLabels:
154 {{- include "gitdata.selectorLabels" . | nindent 6 }}
155 app.kubernetes.io/component: mcp
156{{- end }}
157{{- end }}
Modified chart/values.yaml View chart/values.yaml
@@ -251,6 +251,32 @@
251251 extraEnv: []
252252
253253# ---------------------------------------------------------------------------
254# MCP:独立服务(Streamable HTTP),需要挂载 repos PVC(仓库读取走本地文件系统)。
255# 鉴权:用户访问密钥 + 内部系统密钥(仅内网来源);配置见 APP_MCP__*。
256# ---------------------------------------------------------------------------
257mcp:
258 enabled: true
259 replicaCount: 2
260 port: 3007
261 resources:
262 requests: { cpu: 100m, memory: 128Mi }
263 limits: { memory: 512Mi }
264 extraEnv: []
265 # 独立域名 Ingress(同源提供 /v1 端点与 /healthz)
266 ingress:
267 enabled: false
268 host: mcp.gitdata.example.com
269 className: ""
270 annotations: {}
271 tls:
272 enabled: false
273 secretName: ""
274 clusterIssuer: ""
275 pdb:
276 enabled: false
277 minAvailable: 1
278
279# ---------------------------------------------------------------------------
254280# 存储:githost 与 synchost 共享 bare 仓库,需要 RWX
255281# ---------------------------------------------------------------------------
256282persistence:
Modified config/default.toml View config/default.toml
@@ -382,6 +382,26 @@
382382request_timeout_secs = 10
383383ttl_secs = 86400
384384
385[mcp]
386enabled = true
387bind_addr = "0.0.0.0"
388port = 3007
389# MCP endpoint Host header allowlist. Empty keeps the rmcp default (loopback only).
390# Production must set e.g. ["mcp.gitdata.ai"].
391allowed_hosts = []
392# Only requests whose resolved client IP falls inside these CIDRs can use internal keys.
393internal_cidrs = ["127.0.0.1/32", "::1/128"]
394# Proxies whose X-Forwarded-For header is trusted (e.g. the ingress pod/node CIDR).
395trusted_proxies = []
396
397# Internal system keys ([[mcp.internal_keys]]). Set exactly one of token or token_hash
398# (token_hash = sha256_hex(token), recommended). These act as the platform AI bot with
399# full permissions and are accepted only from internal_cidrs.
400# [[mcp.internal_keys]]
401# name = "gitdata-agent"
402# token_hash = ""
403# enabled = true
404
385405[livekit]
386406enabled = false
387407url = "ws://127.0.0.1:7880"
Modified deploy/values/prod.yaml View deploy/values/prod.yaml
@@ -46,6 +46,16 @@
4646 # 用跨命名空间 FQDN:runner 池在 gitdata-runners 命名空间,这个 URL 会随 claim
4747 # 响应下发(lib/ci/src/actions_payload.rs:87 runtime_url),必须两边都能解析。
4848 APP_CI__RUNTIME_BASE_URL: "http://gitdata-webhook.gitdata.svc.cluster.local:3005"
49 # MCP 独立服务(app/mcp,域名 mcp.gitdata.ai,端点 /v1):
50 # - ALLOWED_HOSTS 必须包含对外域名,否则 rmcp 默认只放行 loopback(fail-closed)。
51 # - 内部系统密钥仅当请求来源 IP 落在 INTERNAL_CIDRS 内才被接受;TRUSTED_PROXIES
52 # 必须覆盖 ingress 出口地址(traefik 节点/Pod CIDR),否则外部请求的来源会以
53 # traefik Pod IP 参与内网判定。
54 # - 内部密钥是机密:APP_MCP__INTERNAL_KEYS 放在 Gitea Secret PROD_SECRET_VALUES,
55 # 形如 '[{name="gitdata-agent",token_hash="<sha256 hex>"}]',不要写在本文件。
56 APP_MCP__ALLOWED_HOSTS: '["mcp.gitdata.ai"]'
57 APP_MCP__INTERNAL_CIDRS: '["10.0.0.0/8","172.16.0.0/12","192.168.0.0/16"]'
58 APP_MCP__TRUSTED_PROXIES: '["10.0.0.0/8"]'
4959 RUST_LOG: "info,session::session_store=error"
5060 OTEL_TRACES_SAMPLER: "parentbased_traceidratio"
5161 OTEL_TRACES_SAMPLER_ARG: "0.2"
@@ -115,9 +125,14 @@
115125
116126adminhost:
117127 enabled: true
128 grpcTrustedKeys: []
129
130mcp:
131 enabled: true
132 replicaCount: 2
118133 ingress:
119134 enabled: true
120 host: admin.gitdata.ai
135 host: mcp.gitdata.ai
121136 className: traefik
122137 tls:
123138 enabled: true
Added lib/admin/src/services/mcp.rs View lib/admin/src/services/mcp.rs
@@ -0,0 +1,72 @@
1use serde::{Deserialize, Serialize};
2use utoipa::ToSchema;
3use uuid::Uuid;
4
5use crate::errors::AdminApiError;
6use crate::services::service::AdminService;
7
8#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
9pub struct AdminMcpToolDto {
10 pub name: String,
11 pub group: String,
12 pub description: String,
13 pub read_only: bool,
14 pub enabled: bool,
15 pub updated_at: Option<String>,
16 pub updated_by: Option<String>,
17}
18
19#[derive(Debug, Clone, Deserialize, ToSchema)]
20pub struct UpdateMcpToolCmd {
21 pub enabled: bool,
22}
23
24impl AdminService {
25 pub async fn list_mcp_tools(&self) -> Vec<AdminMcpToolDto> {
26 let settings = mcp_api::settings::read(&self.state).await;
27 let mut tools: Vec<AdminMcpToolDto> = mcp_api::catalog::tools()
28 .into_iter()
29 .map(|tool| AdminMcpToolDto {
30 enabled: !settings.disabled.iter().any(|name| name == &tool.name),
31 updated_at: settings.updated_at.clone(),
32 updated_by: settings.updated_by.clone(),
33 name: tool.name,
34 group: tool.group,
35 description: tool.description,
36 read_only: tool.read_only,
37 })
38 .collect();
39 tools.sort_by(|a, b| a.group.cmp(&b.group).then_with(|| a.name.cmp(&b.name)));
40 tools
41 }
42
43 pub async fn update_mcp_tool(
44 &self,
45 name: &str,
46 enabled: bool,
47 actor_id: Uuid,
48 ) -> Result<AdminMcpToolDto, AdminApiError> {
49 let name = name.trim();
50 let tool = mcp_api::catalog::tools()
51 .into_iter()
52 .find(|tool| tool.name == name)
53 .ok_or_else(|| AdminApiError::not_found("unknown mcp tool"))?;
54 let mut disabled = mcp_api::settings::read(&self.state).await.disabled;
55 disabled.retain(|existing| existing != name);
56 if !enabled {
57 disabled.push(name.to_string());
58 }
59 let settings = mcp_api::settings::update(&self.state, disabled, Some(actor_id.to_string()))
60 .await
61 .map_err(AdminApiError::internal)?;
62 Ok(AdminMcpToolDto {
63 name: tool.name,
64 group: tool.group,
65 description: tool.description,
66 read_only: tool.read_only,
67 enabled,
68 updated_at: settings.updated_at,
69 updated_by: settings.updated_by,
70 })
71 }
72}
Added lib/config/src/mcp.rs View lib/config/src/mcp.rs
@@ -0,0 +1,150 @@
1use std::collections::HashSet;
2use std::net::IpAddr;
3
4use ipnet::IpNet;
5use serde::{Deserialize, Serialize};
6
7#[derive(Debug, Clone, Serialize, Deserialize)]
8#[serde(default)]
9pub struct McpConfig {
10 pub enabled: Option<bool>,
11 pub bind_addr: Option<String>,
12 pub port: Option<u16>,
13 pub allowed_hosts: Vec<String>,
14 pub internal_cidrs: Vec<String>,
15 pub trusted_proxies: Vec<String>,
16 pub internal_keys: Vec<InternalKeyConfig>,
17}
18
19impl Default for McpConfig {
20 fn default() -> Self {
21 Self {
22 enabled: None,
23 bind_addr: None,
24 port: None,
25 allowed_hosts: Vec::new(),
26 internal_cidrs: vec!["127.0.0.1/32".to_string(), "::1/128".to_string()],
27 trusted_proxies: Vec::new(),
28 internal_keys: Vec::new(),
29 }
30 }
31}
32
33#[derive(Debug, Clone, Default, Serialize, Deserialize)]
34#[serde(default)]
35pub struct InternalKeyConfig {
36 pub name: String,
37 pub token: Option<String>,
38 pub token_hash: Option<String>,
39 pub enabled: Option<bool>,
40}
41
42impl InternalKeyConfig {
43 pub fn enabled(&self) -> bool {
44 self.enabled.unwrap_or(true)
45 }
46}
47
48impl McpConfig {
49 pub fn enabled(&self) -> bool {
50 self.enabled.unwrap_or(true)
51 }
52
53 pub fn bind_addr(&self) -> &str {
54 self.bind_addr.as_deref().unwrap_or("0.0.0.0")
55 }
56
57 pub fn port(&self) -> u16 {
58 self.port.unwrap_or(3007)
59 }
60
61 pub fn is_valid(&self) -> Result<(), String> {
62 if !self.enabled() {
63 return Ok(());
64 }
65 let bind = self.bind_addr();
66 if bind.parse::<IpAddr>().is_err() {
67 return Err(format!("mcp.bind_addr is not a valid IP: {bind:?}"));
68 }
69 if self.port == Some(0) {
70 return Err("mcp.port must be greater than 0".to_string());
71 }
72 for host in &self.allowed_hosts {
73 if host.trim().is_empty() {
74 return Err("mcp.allowed_hosts must not contain empty entries".to_string());
75 }
76 }
77 for cidr in &self.internal_cidrs {
78 parse_net(cidr)
79 .map_err(|e| format!("mcp.internal_cidrs entry {cidr:?} is invalid: {e}"))?;
80 }
81 for cidr in &self.trusted_proxies {
82 parse_net(cidr)
83 .map_err(|e| format!("mcp.trusted_proxies entry {cidr:?} is invalid: {e}"))?;
84 }
85 let mut names = HashSet::new();
86 for key in &self.internal_keys {
87 let name = key.name.trim();
88 if name.is_empty() {
89 return Err("mcp.internal_keys[].name must not be empty".to_string());
90 }
91 if !names.insert(name.to_string()) {
92 return Err(format!("mcp.internal_keys name {name:?} is duplicated"));
93 }
94 match (&key.token, &key.token_hash) {
95 (Some(token), None) => {
96 if token.trim().len() < 16 {
97 return Err(format!(
98 "mcp.internal_keys[{name}].token must be at least 16 characters"
99 ));
100 }
101 }
102 (None, Some(hash)) => {
103 let hash = hash.trim();
104 if hash.len() != 64 || !hash.bytes().all(|b| b.is_ascii_hexdigit()) {
105 return Err(format!(
106 "mcp.internal_keys[{name}].token_hash must be 64 hex characters"
107 ));
108 }
109 }
110 (None, None) => {
111 return Err(format!(
112 "mcp.internal_keys[{name}] must set token or token_hash"
113 ));
114 }
115 (Some(_), Some(_)) => {
116 return Err(format!(
117 "mcp.internal_keys[{name}] must set only one of token or token_hash"
118 ));
119 }
120 }
121 }
122 Ok(())
123 }
124
125 pub fn parsed_internal_cidrs(&self) -> Vec<IpNet> {
126 self.internal_cidrs
127 .iter()
128 .filter_map(|cidr| parse_net(cidr).ok())
129 .collect()
130 }
131
132 pub fn parsed_trusted_proxies(&self) -> Vec<IpNet> {
133 self.trusted_proxies
134 .iter()
135 .filter_map(|cidr| parse_net(cidr).ok())
136 .collect()
137 }
138}
139
140fn parse_net(raw: &str) -> Result<IpNet, String> {
141 let raw = raw.trim();
142 if let Ok(net) = raw.parse::<IpNet>() {
143 return Ok(net);
144 }
145 if let Ok(ip) = raw.parse::<IpAddr>() {
146 let prefix = if ip.is_ipv4() { 32 } else { 128 };
147 return IpNet::new(ip, prefix).map_err(|e| e.to_string());
148 }
149 Err("expected an IP or CIDR".to_string())
150}
Added lib/mcp-api/src/tools/args.rs View lib/mcp-api/src/tools/args.rs
@@ -0,0 +1,1078 @@
1use serde::Deserialize;
2use uuid::Uuid;
3
4#[derive(Debug, Deserialize, schemars::JsonSchema)]
5pub struct SpaceNameArgs {
6 pub space: String,
7}
8
9#[derive(Debug, Deserialize, schemars::JsonSchema)]
10pub struct RepoRefArgs {
11 pub space: String,
12 pub repo: String,
13}
14
15#[derive(Debug, Deserialize, schemars::JsonSchema)]
16pub struct UserRefArgs {
17 #[serde(default)]
18 pub user_id: Option<Uuid>,
19 #[serde(default)]
20 pub username: Option<String>,
21}
22
23#[derive(Debug, Deserialize, schemars::JsonSchema)]
24pub struct UserIdsArgs {
25 pub ids: Vec<Uuid>,
26}
27
28#[derive(Debug, Default, Deserialize, schemars::JsonSchema)]
29pub struct SpaceListArgs {
30 #[serde(default)]
31 pub limit: Option<u64>,
32 #[serde(default)]
33 pub offset: Option<u64>,
34}
35
36#[derive(Debug, Default, Deserialize, schemars::JsonSchema)]
37pub struct SpaceExploreArgs {
38 #[serde(default)]
39 pub q: Option<String>,
40 #[serde(default)]
41 pub sort: Option<String>,
42 #[serde(default)]
43 pub limit: Option<u64>,
44 #[serde(default)]
45 pub offset: Option<u64>,
46}
47
48#[derive(Debug, Deserialize, schemars::JsonSchema)]
49pub struct SpaceCreateArgs {
50 pub name: String,
51 #[serde(default)]
52 pub description: Option<String>,
53 #[serde(default)]
54 pub is_private: bool,
55 #[serde(default)]
56 pub allow_join: Option<bool>,
57 #[serde(default)]
58 pub allow_invite: Option<bool>,
59 #[serde(default)]
60 pub welcome_message: Option<String>,
61 #[serde(default)]
62 pub allow_join_level: Option<String>,
63 #[serde(default)]
64 pub member_must_enable_2fa: bool,
65}
66
67#[derive(Debug, Deserialize, schemars::JsonSchema)]
68pub struct SpaceUpdateArgs {
69 pub space: String,
70 #[serde(default)]
71 pub name: Option<String>,
72 #[serde(default)]
73 pub description: Option<String>,
74 #[serde(default)]
75 pub is_private: Option<bool>,
76 #[serde(default)]
77 pub allow_join: Option<bool>,
78 #[serde(default)]
79 pub allow_invite: Option<bool>,
80 #[serde(default)]
81 pub welcome_message: Option<String>,
82 #[serde(default)]
83 pub allow_join_level: Option<String>,
84 #[serde(default)]
85 pub member_must_enable_2fa: Option<bool>,
86}
87
88#[derive(Debug, Deserialize, schemars::JsonSchema)]
89pub struct SpaceMemberListArgs {
90 pub space: String,
91 #[serde(default)]
92 pub limit: Option<u64>,
93 #[serde(default)]
94 pub offset: Option<u64>,
95}
96
97#[derive(Debug, Deserialize, schemars::JsonSchema)]
98pub struct SpaceMemberAddArgs {
99 pub space: String,
100 pub user_id: Uuid,
101 #[serde(default)]
102 pub alias_name: Option<String>,
103}
104
105#[derive(Debug, Deserialize, schemars::JsonSchema)]
106pub struct SpaceMemberUpdateArgs {
107 pub space: String,
108 pub user_id: Uuid,
109 #[serde(default)]
110 pub alias_name: Option<String>,
111 #[serde(default)]
112 pub muted: Option<bool>,
113}
114
115#[derive(Debug, Deserialize, schemars::JsonSchema)]
116pub struct SpaceMemberRefArgs {
117 pub space: String,
118 pub user_id: Uuid,
119}
120
121#[derive(Debug, Deserialize, schemars::JsonSchema)]
122pub struct SpaceGroupListArgs {
123 pub space: String,
124 #[serde(default)]
125 pub limit: Option<u64>,
126 #[serde(default)]
127 pub offset: Option<u64>,
128}
129
130#[derive(Debug, Deserialize, schemars::JsonSchema)]
131pub struct SpaceGroupCreateArgs {
132 pub space: String,
133 pub name: String,
134 #[serde(default)]
135 pub color: Option<String>,
136 #[serde(default)]
137 pub position: Option<i64>,
138 #[serde(default)]
139 pub permissions: Vec<String>,
140}
141
142#[derive(Debug, Deserialize, schemars::JsonSchema)]
143pub struct SpaceGroupUpdateArgs {
144 pub space: String,
145 pub group_id: Uuid,
146 #[serde(default)]
147 pub name: Option<String>,
148 #[serde(default)]
149 pub color: Option<String>,
150 #[serde(default)]
151 pub position: Option<i64>,
152 #[serde(default)]
153 pub permissions: Option<Vec<String>>,
154}
155
156#[derive(Debug, Deserialize, schemars::JsonSchema)]
157pub struct SpaceGroupRefArgs {
158 pub space: String,
159 pub group_id: Uuid,
160}
161
162#[derive(Debug, Deserialize, schemars::JsonSchema)]
163pub struct SpaceGroupAssignArgs {
164 pub space: String,
165 pub user_id: Uuid,
166 pub group_id: Uuid,
167}
168
169#[derive(Debug, Deserialize, schemars::JsonSchema)]
170pub struct SpaceDirTreeArgs {
171 pub space: String,
172 #[serde(default)]
173 pub parent_id: Option<Uuid>,
174}
175
176#[derive(Debug, Deserialize, schemars::JsonSchema)]
177pub struct SpaceDirCreateArgs {
178 pub space: String,
179 pub name: String,
180 #[serde(default)]
181 pub parent_id: Option<Uuid>,
182}
183
184#[derive(Debug, Deserialize, schemars::JsonSchema)]
185pub struct SpaceDirUpdateArgs {
186 pub space: String,
187 pub dir_id: Uuid,
188 pub name: String,
189}
190
191#[derive(Debug, Deserialize, schemars::JsonSchema)]
192pub struct SpaceDirMoveArgs {
193 pub space: String,
194 pub dir_id: Uuid,
195 #[serde(default)]
196 pub parent_id: Option<Uuid>,
197 #[serde(default)]
198 pub position: Option<i64>,
199}
200
201#[derive(Debug, Deserialize, schemars::JsonSchema)]
202pub struct SpaceInviteListArgs {
203 pub space: String,
204 #[serde(default)]
205 pub limit: Option<u64>,
206 #[serde(default)]
207 pub offset: Option<u64>,
208}
209
210#[derive(Debug, Deserialize, schemars::JsonSchema)]
211pub struct SpaceInviteCreateArgs {
212 pub space: String,
213 #[serde(default)]
214 pub user_id: Option<Uuid>,
215 #[serde(default)]
216 pub email: Option<String>,
217 #[serde(default)]
218 pub group_ids: Vec<Uuid>,
219 #[serde(default)]
220 pub expires_at: Option<String>,
221}
222
223#[derive(Debug, Deserialize, schemars::JsonSchema)]
224pub struct SpaceInviteRefArgs {
225 pub space: String,
226 pub invite_id: Uuid,
227}
228
229#[derive(Debug, Deserialize, schemars::JsonSchema)]
230pub struct SpaceJoinRequestListArgs {
231 pub space: String,
232 #[serde(default)]
233 pub status: Option<String>,
234 #[serde(default)]
235 pub limit: Option<u64>,
236 #[serde(default)]
237 pub offset: Option<u64>,
238}
239
240#[derive(Debug, Deserialize, schemars::JsonSchema)]
241pub struct SpaceJoinRequestCreateArgs {
242 pub space: String,
243 #[serde(default)]
244 pub note: Option<String>,
245}
246
247#[derive(Debug, Deserialize, schemars::JsonSchema)]
248pub struct SpaceJoinRequestRefArgs {
249 pub space: String,
250 pub request_id: Uuid,
251}
252
253#[derive(Debug, Default, Deserialize, schemars::JsonSchema)]
254pub struct RepoListArgs {
255 pub space: String,
256 #[serde(default)]
257 pub limit: Option<u64>,
258 #[serde(default)]
259 pub offset: Option<u64>,
260 #[serde(default)]
261 pub sort: Option<String>,
262}
263
264#[derive(Debug, Deserialize, schemars::JsonSchema)]
265pub struct RepoCreateArgs {
266 pub space: String,
267 pub name: String,
268 #[serde(default)]
269 pub description: Option<String>,
270 #[serde(default)]
271 pub is_private: bool,
272 #[serde(default)]
273 pub default_branch: Option<String>,
274 #[serde(default)]
275 pub parent_dir_id: Option<Uuid>,
276 #[serde(default)]
277 pub enable_lfs: bool,
278 #[serde(default)]
279 pub license_name: Option<String>,
280 #[serde(default)]
281 pub init_message: Option<String>,
282}
283
284#[derive(Debug, Deserialize, schemars::JsonSchema)]
285pub struct RepoUpdateArgs {
286 pub space: String,
287 pub repo: String,
288 #[serde(default)]
289 pub description: Option<String>,
290 #[serde(default)]
291 pub website: Option<String>,
292 #[serde(default)]
293 pub is_private: Option<bool>,
294 #[serde(default)]
295 pub enable_lfs: Option<bool>,
296 #[serde(default)]
297 pub default_branch: Option<String>,
298 #[serde(default)]
299 pub ai_review_on_push: Option<bool>,
300 #[serde(default)]
301 pub ai_summary_on_push: Option<bool>,
302 #[serde(default)]
303 pub ai_review_on_pull: Option<bool>,
304}
305
306#[derive(Debug, Deserialize, schemars::JsonSchema)]
307pub struct RepoArchiveArgs {
308 pub space: String,
309 pub repo: String,
310 pub archived: bool,
311}
312
313#[derive(Debug, Deserialize, schemars::JsonSchema)]
314pub struct RepoBranchGetArgs {
315 pub space: String,
316 pub repo: String,
317 pub branch: String,
318}
319
320#[derive(Debug, Deserialize, schemars::JsonSchema)]
321pub struct RepoBranchCreateArgs {
322 pub space: String,
323 pub repo: String,
324 pub name: String,
325 #[serde(default)]
326 pub start_point: Option<String>,
327}
328
329#[derive(Debug, Deserialize, schemars::JsonSchema)]
330pub struct RepoBranchRenameArgs {
331 pub space: String,
332 pub repo: String,
333 pub from: String,
334 pub to: String,
335}
336
337#[derive(Debug, Deserialize, schemars::JsonSchema)]
338pub struct RepoBranchDeleteArgs {
339 pub space: String,
340 pub repo: String,
341 pub name: String,
342 #[serde(default)]
343 pub force: bool,
344}
345
346#[derive(Debug, Deserialize, schemars::JsonSchema)]
347pub struct RepoBranchDefaultArgs {
348 pub space: String,
349 pub repo: String,
350 pub name: String,
351}
352
353#[derive(Debug, Deserialize, schemars::JsonSchema)]
354pub struct RepoTagGetArgs {
355 pub space: String,
356 pub repo: String,
357 pub tag: String,
358}
359
360#[derive(Debug, Deserialize, schemars::JsonSchema)]
361pub struct RepoTagCreateArgs {
362 pub space: String,
363 pub repo: String,
364 pub name: String,
365 pub target: String,
366 #[serde(default)]
367 pub message: Option<String>,
368 #[serde(default)]
369 pub annotated: bool,
370}
371
372#[derive(Debug, Deserialize, schemars::JsonSchema)]
373pub struct RepoTagDeleteArgs {
374 pub space: String,
375 pub repo: String,
376 pub name: String,
377}
378
379#[derive(Debug, Deserialize, schemars::JsonSchema)]
380pub struct RepoCommitListArgs {
381 pub space: String,
382 pub repo: String,
383 #[serde(default)]
384 pub revision: Option<String>,
385 #[serde(default)]
386 pub path: Option<String>,
387 #[serde(default)]
388 pub max_count: Option<u32>,
389 #[serde(default)]
390 pub skip: Option<u32>,
391 #[serde(default)]
392 pub first_parent: bool,
393}
394
395#[derive(Debug, Deserialize, schemars::JsonSchema)]
396pub struct RepoCommitGetArgs {
397 pub space: String,
398 pub repo: String,
399 pub revision: String,
400}
401
402#[derive(Debug, Deserialize, schemars::JsonSchema)]
403pub struct RepoTreeArgs {
404 pub space: String,
405 pub repo: String,
406 #[serde(default)]
407 pub revision: Option<String>,
408 #[serde(default)]
409 pub path: Option<String>,
410 #[serde(default)]
411 pub recurse: bool,
412}
413
414#[derive(Debug, Deserialize, schemars::JsonSchema)]
415pub struct RepoFileArgs {
416 pub space: String,
417 pub repo: String,
418 pub path: String,
419 #[serde(default)]
420 pub revision: Option<String>,
421}
422
423#[derive(Debug, Default, Deserialize, schemars::JsonSchema)]
424pub struct RepoReleaseListArgs {
425 pub space: String,
426 pub repo: String,
427 #[serde(default)]
428 pub draft: Option<bool>,
429 #[serde(default)]
430 pub limit: Option<u64>,
431 #[serde(default)]
432 pub offset: Option<u64>,
433}
434
435#[derive(Debug, Deserialize, schemars::JsonSchema)]
436pub struct RepoReleaseGetArgs {
437 pub space: String,
438 pub repo: String,
439 pub tag: String,
440}
441
442#[derive(Debug, Deserialize, schemars::JsonSchema)]
443pub struct RepoReleaseCreateArgs {
444 pub space: String,
445 pub repo: String,
446 pub tag_name: String,
447 #[serde(default)]
448 pub target: Option<String>,
449 #[serde(default)]
450 pub title: Option<String>,
451 #[serde(default)]
452 pub content: Option<String>,
453 #[serde(default)]
454 pub is_draft: Option<bool>,
455 #[serde(default)]
456 pub is_prerelease: Option<bool>,
457}
458
459#[derive(Debug, Deserialize, schemars::JsonSchema)]
460pub struct RepoReleaseUpdateArgs {
461 pub space: String,
462 pub repo: String,
463 pub tag: String,
464 #[serde(default)]
465 pub title: Option<String>,
466 #[serde(default)]
467 pub content: Option<String>,
468 #[serde(default)]
469 pub is_draft: Option<bool>,
470 #[serde(default)]
471 pub is_prerelease: Option<bool>,
472}
473
474#[derive(Debug, Deserialize, schemars::JsonSchema)]
475pub struct RepoReleaseDeleteArgs {
476 pub space: String,
477 pub repo: String,
478 pub tag: String,
479}
480
481#[derive(Debug, Deserialize, schemars::JsonSchema)]
482pub struct RepoTopicArgs {
483 pub space: String,
484 pub repo: String,
485}
486
487#[derive(Debug, Deserialize, schemars::JsonSchema)]
488pub struct RepoTopicChangeArgs {
489 pub space: String,
490 pub repo: String,
491 pub topic: String,
492}
493
494#[derive(Debug, Deserialize, schemars::JsonSchema)]
495pub struct RepoWatchArgs {
496 pub space: String,
497 pub repo: String,
498 pub mode: String,
499}
500
501#[derive(Debug, Deserialize, schemars::JsonSchema)]
502pub struct RepoStargazerListArgs {
503 pub space: String,
504 pub repo: String,
505 #[serde(default)]
506 pub limit: Option<u64>,
507 #[serde(default)]
508 pub offset: Option<u64>,
509}
510
511#[derive(Debug, Deserialize, schemars::JsonSchema)]
512pub struct RepoWatcherListArgs {
513 pub space: String,
514 pub repo: String,
515 #[serde(default)]
516 pub mode: Option<String>,
517 #[serde(default)]
518 pub limit: Option<u64>,
519 #[serde(default)]
520 pub offset: Option<u64>,
521}
522
523#[derive(Debug, Default, Deserialize, schemars::JsonSchema)]
524pub struct IssueBoardListArgs {
525 pub space: String,
526 #[serde(default)]
527 pub dir_id: Option<Uuid>,
528 #[serde(default)]
529 pub q: Option<String>,
530 #[serde(default)]
531 pub limit: Option<u64>,
532 #[serde(default)]
533 pub offset: Option<u64>,
534}
535
536#[derive(Debug, Deserialize, schemars::JsonSchema)]
537pub struct IssueBoardCreateArgs {
538 pub space: String,
539 pub name: String,
540 #[serde(default)]
541 pub parent_dir_id: Option<Uuid>,
542 #[serde(default)]
543 pub icon: Option<String>,
544 #[serde(default)]
545 pub description: Option<String>,
546}
547
548#[derive(Debug, Deserialize, schemars::JsonSchema)]
549pub struct IssueBoardUpdateArgs {
550 pub space: String,
551 pub board_id: Uuid,
552 #[serde(default)]
553 pub name: Option<String>,
554 #[serde(default)]
555 pub icon: Option<String>,
556 #[serde(default)]
557 pub description: Option<String>,
558}
559
560#[derive(Debug, Deserialize, schemars::JsonSchema)]
561pub struct IssueBoardRefArgs {
562 pub space: String,
563 pub board_id: Uuid,
564}
565
566#[derive(Debug, Default, Deserialize, schemars::JsonSchema)]
567pub struct IssueListArgs {
568 pub space: String,
569 pub board_id: Uuid,
570 #[serde(default)]
571 pub state: Option<String>,
572 #[serde(default)]
573 pub author_id: Option<Uuid>,
574 #[serde(default)]
575 pub milestone_id: Option<Uuid>,
576 #[serde(default)]
577 pub label_id: Option<Uuid>,
578 #[serde(default)]
579 pub assignee_id: Option<Uuid>,
580 #[serde(default)]
581 pub priority: Option<String>,
582 #[serde(default)]
583 pub sort: Option<String>,
584 #[serde(default)]
585 pub q: Option<String>,
586 #[serde(default)]
587 pub limit: Option<u64>,
588 #[serde(default)]
589 pub offset: Option<u64>,
590}
591
592#[derive(Debug, Deserialize, schemars::JsonSchema)]
593pub struct IssueRefArgs {
594 pub space: String,
595 pub board_id: Uuid,
596 pub issue_id: String,
597}
598
599#[derive(Debug, Deserialize, schemars::JsonSchema)]
600pub struct IssueCreateArgs {
601 pub space: String,
602 pub board_id: Uuid,
603 pub title: String,
604 #[serde(default)]
605 pub description: Option<String>,
606 #[serde(default)]
607 pub content: Option<String>,
608 #[serde(default)]
609 pub milestone_id: Option<Uuid>,
610 #[serde(default)]
611 pub priority: Option<String>,
612 #[serde(default)]
613 pub due_at: Option<String>,
614 #[serde(default)]
615 pub assignee_ids: Vec<Uuid>,
616 #[serde(default)]
617 pub label_ids: Vec<Uuid>,
618}
619
620#[derive(Debug, Deserialize, schemars::JsonSchema)]
621pub struct IssueUpdateArgs {
622 pub space: String,
623 pub board_id: Uuid,
624 pub issue_id: String,
625 #[serde(default)]
626 pub title: Option<String>,
627 #[serde(default)]
628 pub description: Option<String>,
629 #[serde(default)]
630 pub content: Option<String>,
631 #[serde(default)]
632 pub milestone_id: Option<Uuid>,
633 #[serde(default)]
634 pub priority: Option<String>,
635 #[serde(default)]
636 pub due_at: Option<String>,
637 #[serde(default)]
638 pub assignee_ids: Option<Vec<Uuid>>,
639 #[serde(default)]
640 pub label_ids: Option<Vec<Uuid>>,
641}
642
643#[derive(Debug, Default, Deserialize, schemars::JsonSchema)]
644pub struct IssueCommentListArgs {
645 pub space: String,
646 pub board_id: Uuid,
647 pub issue_id: String,
648 #[serde(default)]
649 pub parent_id: Option<Uuid>,
650 #[serde(default)]
651 pub sort: Option<String>,
652 #[serde(default)]
653 pub limit: Option<u64>,
654 #[serde(default)]
655 pub offset: Option<u64>,
656}
657
658#[derive(Debug, Deserialize, schemars::JsonSchema)]
659pub struct IssueCommentCreateArgs {
660 pub space: String,
661 pub board_id: Uuid,
662 pub issue_id: String,
663 pub content: String,
664 #[serde(default)]
665 pub parent_id: Option<Uuid>,
666}
667
668#[derive(Debug, Deserialize, schemars::JsonSchema)]
669pub struct IssueCommentUpdateArgs {
670 pub space: String,
671 pub board_id: Uuid,
672 pub issue_id: String,
673 pub comment_id: Uuid,
674 pub content: String,
675}
676
677#[derive(Debug, Deserialize, schemars::JsonSchema)]
678pub struct IssueCommentRefArgs {
679 pub space: String,
680 pub board_id: Uuid,
681 pub issue_id: String,
682 pub comment_id: Uuid,
683}
684
685#[derive(Debug, Default, Deserialize, schemars::JsonSchema)]
686pub struct IssueLabelListArgs {
687 pub space: String,
688 pub board_id: Uuid,
689 #[serde(default)]
690 pub q: Option<String>,
691 #[serde(default)]
692 pub limit: Option<u64>,
693 #[serde(default)]
694 pub offset: Option<u64>,
695}
696
697#[derive(Debug, Deserialize, schemars::JsonSchema)]
698pub struct IssueLabelCreateArgs {
699 pub space: String,
700 pub board_id: Uuid,
701 pub name: String,
702 pub color: String,
703 #[serde(default)]
704 pub exclusive: Option<bool>,
705 #[serde(default)]
706 pub description: Option<String>,
707}
708
709#[derive(Debug, Deserialize, schemars::JsonSchema)]
710pub struct IssueLabelUpdateArgs {
711 pub space: String,
712 pub board_id: Uuid,
713 pub label_id: Uuid,
714 #[serde(default)]
715 pub name: Option<String>,
716 #[serde(default)]
717 pub color: Option<String>,
718 #[serde(default)]
719 pub exclusive: Option<bool>,
720 #[serde(default)]
721 pub description: Option<String>,
722 #[serde(default)]
723 pub is_archived: Option<bool>,
724}
725
726#[derive(Debug, Deserialize, schemars::JsonSchema)]
727pub struct IssueLabelRefArgs {
728 pub space: String,
729 pub board_id: Uuid,
730 pub label_id: Uuid,
731}
732
733#[derive(Debug, Deserialize, schemars::JsonSchema)]
734pub struct IssueLabelBindArgs {
735 pub space: String,
736 pub board_id: Uuid,
737 pub issue_id: String,
738 pub label_id: Uuid,
739}
740
741#[derive(Debug, Default, Deserialize, schemars::JsonSchema)]
742pub struct IssueMilestoneListArgs {
743 pub space: String,
744 pub board_id: Uuid,
745 #[serde(default)]
746 pub is_closed: Option<bool>,
747 #[serde(default)]
748 pub q: Option<String>,
749 #[serde(default)]
750 pub limit: Option<u64>,
751 #[serde(default)]
752 pub offset: Option<u64>,
753}
754
755#[derive(Debug, Deserialize, schemars::JsonSchema)]
756pub struct IssueMilestoneCreateArgs {
757 pub space: String,
758 pub board_id: Uuid,
759 pub name: String,
760 #[serde(default)]
761 pub description: Option<String>,
762 #[serde(default)]
763 pub due_at: Option<String>,
764}
765
766#[derive(Debug, Deserialize, schemars::JsonSchema)]
767pub struct IssueMilestoneUpdateArgs {
768 pub space: String,
769 pub board_id: Uuid,
770 pub milestone_id: Uuid,
771 #[serde(default)]
772 pub name: Option<String>,
773 #[serde(default)]
774 pub description: Option<String>,
775 #[serde(default)]
776 pub due_at: Option<String>,
777 #[serde(default)]
778 pub is_closed: Option<bool>,
779}
780
781#[derive(Debug, Deserialize, schemars::JsonSchema)]
782pub struct IssueMilestoneRefArgs {
783 pub space: String,
784 pub board_id: Uuid,
785 pub milestone_id: Uuid,
786}
787
788#[derive(Debug, Default, Deserialize, schemars::JsonSchema)]
789pub struct IssueMemberListArgs {
790 pub space: String,
791 pub board_id: Uuid,
792 pub issue_id: String,
793 #[serde(default)]
794 pub limit: Option<u64>,
795 #[serde(default)]
796 pub offset: Option<u64>,
797}
798
799#[derive(Debug, Deserialize, schemars::JsonSchema)]
800pub struct IssueMemberCreateArgs {
801 pub space: String,
802 pub board_id: Uuid,
803 pub issue_id: String,
804 pub user_id: Uuid,
805 pub kind: String,
806}
807
808#[derive(Debug, Deserialize, schemars::JsonSchema)]
809pub struct IssueMemberRefArgs {
810 pub space: String,
811 pub board_id: Uuid,
812 pub issue_id: String,
813 pub member_id: Uuid,
814}
815
816#[derive(Debug, Deserialize, schemars::JsonSchema)]
817pub struct IssuePinListArgs {
818 pub space: String,
819 pub board_id: Uuid,
820}
821
822#[derive(Debug, Deserialize, schemars::JsonSchema)]
823pub struct IssuePinCreateArgs {
824 pub space: String,
825 pub board_id: Uuid,
826 pub issue_id: String,
827 #[serde(default)]
828 pub pin_order: Option<i32>,
829}
830
831#[derive(Debug, Deserialize, schemars::JsonSchema)]
832pub struct IssuePinUpdateArgs {
833 pub space: String,
834 pub board_id: Uuid,
835 pub pin_id: Uuid,
836 pub pin_order: i32,
837}
838
839#[derive(Debug, Deserialize, schemars::JsonSchema)]
840pub struct IssuePinRefArgs {
841 pub space: String,
842 pub board_id: Uuid,
843 pub pin_id: Uuid,
844}
845
846#[derive(Debug, Deserialize, schemars::JsonSchema)]
847pub struct IssueDependencyListArgs {
848 pub space: String,
849 pub board_id: Uuid,
850 pub issue_id: String,
851}
852
853#[derive(Debug, Deserialize, schemars::JsonSchema)]
854pub struct IssueDependencyCreateArgs {
855 pub space: String,
856 pub board_id: Uuid,
857 pub issue_id: String,
858 pub dependency_id: Uuid,
859}
860
861#[derive(Debug, Deserialize, schemars::JsonSchema)]
862pub struct IssueDependencyRefArgs {
863 pub space: String,
864 pub board_id: Uuid,
865 pub issue_id: String,
866 pub dep_id: Uuid,
867}
868
869#[derive(Debug, Deserialize, schemars::JsonSchema)]
870pub struct PullListArgs {
871 pub space: String,
872 pub repo: String,
873 #[serde(default)]
874 pub state: Option<String>,
875 #[serde(default)]
876 pub author: Option<String>,
877 #[serde(default)]
878 pub base: Option<String>,
879 #[serde(default)]
880 pub head: Option<String>,
881 #[serde(default)]
882 pub draft: Option<bool>,
883 #[serde(default)]
884 pub search: Option<String>,
885 #[serde(default)]
886 pub sort: Option<String>,
887 #[serde(default)]
888 pub order: Option<String>,
889 #[serde(default)]
890 pub limit: Option<u64>,
891 #[serde(default)]
892 pub offset: Option<u64>,
893}
894
895#[derive(Debug, Deserialize, schemars::JsonSchema)]
896pub struct PullCreateArgs {
897 pub space: String,
898 pub repo: String,
899 pub title: String,
900 pub head_branch: String,
901 #[serde(default)]
902 pub body: Option<String>,
903 #[serde(default)]
904 pub base_branch: Option<String>,
905 #[serde(default)]
906 pub draft: Option<bool>,
907 #[serde(default)]
908 pub issue_id: Option<Uuid>,
909}
910
911#[derive(Debug, Deserialize, schemars::JsonSchema)]
912pub struct PullRefArgs {
913 pub space: String,
914 pub repo: String,
915 pub pull: String,
916}
917
918#[derive(Debug, Deserialize, schemars::JsonSchema)]
919pub struct PullUpdateArgs {
920 pub space: String,
921 pub repo: String,
922 pub pull: String,
923 #[serde(default)]
924 pub title: Option<String>,
925 #[serde(default)]
926 pub body: Option<String>,
927 #[serde(default)]
928 pub base_branch: Option<String>,
929 #[serde(default)]
930 pub draft: Option<bool>,
931 #[serde(default)]
932 pub auto_merge: Option<bool>,
933}
934
935#[derive(Debug, Deserialize, schemars::JsonSchema)]
936pub struct PullMergeArgs {
937 pub space: String,
938 pub repo: String,
939 pub pull: String,
940 #[serde(default)]
941 pub method: Option<String>,
942 #[serde(default)]
943 pub title: Option<String>,
944 #[serde(default)]
945 pub message: Option<String>,
946 #[serde(default)]
947 pub fast_forward_only: Option<bool>,
948 #[serde(default)]
949 pub expect_head_sha: Option<String>,
950 #[serde(default)]
951 pub delete_branch_after_merge: Option<bool>,
952}
953
954#[derive(Debug, Default, Deserialize, schemars::JsonSchema)]
955pub struct PullCommentListArgs {
956 pub space: String,
957 pub repo: String,
958 pub pull: String,
959 #[serde(default)]
960 pub limit: Option<u64>,
961 #[serde(default)]
962 pub offset: Option<u64>,
963}
964
965#[derive(Debug, Deserialize, schemars::JsonSchema)]
966pub struct PullCommentCreateArgs {
967 pub space: String,
968 pub repo: String,
969 pub pull: String,
970 pub body: String,
971 #[serde(default)]
972 pub path: Option<String>,
973 #[serde(default)]
974 pub line: Option<i32>,
975 #[serde(default)]
976 pub side: Option<String>,
977 #[serde(default)]
978 pub commit_sha: Option<String>,
979 #[serde(default)]
980 pub parent_comment_id: Option<Uuid>,
981}
982
983#[derive(Debug, Deserialize, schemars::JsonSchema)]
984pub struct PullCommentUpdateArgs {
985 pub space: String,
986 pub repo: String,
987 pub pull: String,
988 pub comment_id: Uuid,
989 pub body: String,
990}
991
992#[derive(Debug, Deserialize, schemars::JsonSchema)]
993pub struct PullCommentRefArgs {
994 pub space: String,
995 pub repo: String,
996 pub pull: String,
997 pub comment_id: Uuid,
998}
999
1000#[derive(Debug, Deserialize, schemars::JsonSchema)]
1001pub struct PullCommentResolveArgs {
1002 pub space: String,
1003 pub repo: String,
1004 pub pull: String,
1005 pub comment_id: Uuid,
1006 pub resolved: bool,
1007}
1008
1009#[derive(Debug, Deserialize, schemars::JsonSchema)]
1010pub struct PullReviewsArgs {
1011 pub space: String,
1012 pub repo: String,
1013 pub pull: String,
1014}
1015
1016#[derive(Debug, Deserialize, schemars::JsonSchema)]
1017pub struct PullReviewSubmitArgs {
1018 pub space: String,
1019 pub repo: String,
1020 pub pull: String,
1021 pub state: String,
1022 #[serde(default)]
1023 pub body: Option<String>,
1024}
1025
1026#[derive(Debug, Deserialize, schemars::JsonSchema)]
1027pub struct PullReviewDismissArgs {
1028 pub space: String,
1029 pub repo: String,
1030 pub pull: String,
1031 pub review_id: Uuid,
1032}
1033
1034#[derive(Debug, Deserialize, schemars::JsonSchema)]
1035pub struct PullReviewerRequestArgs {
1036 pub space: String,
1037 pub repo: String,
1038 pub pull: String,
1039 pub reviewers: Vec<String>,
1040 #[serde(default)]
1041 pub required: Option<bool>,
1042}
1043
1044#[derive(Debug, Deserialize, schemars::JsonSchema)]
1045pub struct PullReviewerCancelArgs {
1046 pub space: String,
1047 pub repo: String,
1048 pub pull: String,
1049 pub reviewer_id: Uuid,
1050}
1051
1052#[derive(Debug, Deserialize, schemars::JsonSchema)]
1053pub struct PullAssigneeArgs {
1054 pub space: String,
1055 pub repo: String,
1056 pub pull: String,
1057 pub user: String,
1058}
1059
1060#[derive(Debug, Deserialize, schemars::JsonSchema)]
1061pub struct PullLabelArgs {
1062 pub space: String,
1063 pub repo: String,
1064 pub pull: String,
1065 pub label: String,
1066}
1067
1068#[derive(Debug, Default, Deserialize, schemars::JsonSchema)]
1069pub struct PullInboxArgs {
1070 /// created | review-requested | reviewed
1071 pub scope: String,
1072 #[serde(default)]
1073 pub state: Option<String>,
1074 #[serde(default)]
1075 pub limit: Option<u64>,
1076 #[serde(default)]
1077 pub offset: Option<u64>,
1078}
Added lib/mcp-api/src/tools/helpers.rs View lib/mcp-api/src/tools/helpers.rs
@@ -0,0 +1,67 @@
1use git::context::facade::GitContext;
2use rmcp::model::CallToolResult;
3use uuid::Uuid;
4
5use crate::error::{tool_error, tool_message};
6use crate::services::McpServices;
7
8pub(crate) fn page(
9 limit: Option<u64>,
10 offset: Option<u64>,
11 sort: Option<String>,
12) -> export::pagination::PageQuery {
13 export::pagination::PageQuery {
14 limit,
15 offset,
16 sort,
17 }
18}
19
20pub(crate) fn parse_time(raw: &str) -> Result<time::OffsetDateTime, CallToolResult> {
21 time::OffsetDateTime::parse(raw.trim(), &time::format_description::well_known::Rfc3339).map_err(
22 |_| {
23 tool_message(format!(
24 "时间格式无效(需要 RFC3339,如 2025-01-01T00:00:00Z): {raw}"
25 ))
26 },
27 )
28}
29
30pub(crate) fn parse_enum<T: serde::de::DeserializeOwned>(raw: &str) -> Result<T, CallToolResult> {
31 serde_json::from_value(serde_json::Value::String(raw.trim().to_string()))
32 .map_err(|_| tool_message(format!("无效的枚举值: {raw}")))
33}
34
35pub(crate) async fn repo_read(
36 services: &McpServices,
37 uid: Uuid,
38 space: &str,
39 repo: &str,
40) -> Result<GitContext, CallToolResult> {
41 let ctx = services
42 .git
43 .context(space.trim().to_string(), repo.trim().to_string())
44 .await
45 .map_err(tool_error)?;
46 git::share::access::authorize_repo_access(&services.git, uid, &ctx, false, false)
47 .await
48 .map_err(tool_error)?;
49 Ok(ctx)
50}
51
52pub(crate) async fn repo_write(
53 services: &McpServices,
54 uid: Uuid,
55 space: &str,
56 repo: &str,
57) -> Result<GitContext, CallToolResult> {
58 let ctx = services
59 .git
60 .context(space.trim().to_string(), repo.trim().to_string())
61 .await
62 .map_err(tool_error)?;
63 git::share::access::authorize_repo_access(&services.git, uid, &ctx, true, false)
64 .await
65 .map_err(tool_error)?;
66 Ok(ctx)
67}
Added lib/mcp-api/src/tools/issue.rs View lib/mcp-api/src/tools/issue.rs
@@ -0,0 +1,1123 @@
1use issues::services::boards::{CreateBoardCmd, ListBoardQuery, UpdateBoardCmd};
2use issues::services::comments::{CreateCommentCmd, ListCommentQuery, UpdateCommentCmd};
3use issues::services::dependencies::CreateDependencyCmd;
4use issues::services::issues::{CreateIssueCmd, IssueState, ListIssueQuery, UpdateIssueCmd};
5use issues::services::labels::{CreateLabelCmd, ListLabelQuery, UpdateLabelCmd};
6use issues::services::members::{CreateIssueMemberCmd, ListIssueMemberQuery};
7use issues::services::milestones::{CreateMilestoneCmd, ListMilestoneQuery, UpdateMilestoneCmd};
8use issues::services::pins::{CreatePinCmd, UpdatePinCmd};
9use models::issue::issue_item::IssuePriority;
10use models::issue::issue_member::IssueMemberKind;
11use rmcp::model::CallToolResult;
12use uuid::Uuid;
13
14use crate::auth::Principal;
15use crate::error::{tool_done, tool_error, tool_ok, write_denied};
16use crate::services::McpServices;
17use crate::tools::args;
18use crate::tools::helpers::{parse_enum, parse_time};
19
20async fn resolve_issue(
21 services: &McpServices,
22 principal: &Principal,
23 space: &str,
24 board_id: Uuid,
25 reference: &str,
26) -> Result<Uuid, CallToolResult> {
27 services
28 .issues
29 .resolve_issue_ref(principal.uid, space.trim(), board_id, reference.trim())
30 .await
31 .map_err(tool_error)
32}
33
34pub(crate) async fn board_list(
35 services: &McpServices,
36 principal: &Principal,
37 args: args::IssueBoardListArgs,
38) -> CallToolResult {
39 let query = ListBoardQuery {
40 dir_id: args.dir_id,
41 q: args.q,
42 limit: args.limit,
43 offset: args.offset,
44 };
45 match services
46 .issues
47 .list_boards(principal.uid, args.space.trim(), query)
48 .await
49 {
50 Ok(result) => tool_ok(&result),
51 Err(error) => tool_error(error),
52 }
53}
54
55pub(crate) async fn board_get(
56 services: &McpServices,
57 principal: &Principal,
58 args: args::IssueBoardRefArgs,
59) -> CallToolResult {
60 match services
61 .issues
62 .get_board(principal.uid, args.space.trim(), args.board_id)
63 .await
64 {
65 Ok(result) => tool_ok(&result),
66 Err(error) => tool_error(error),
67 }
68}
69
70pub(crate) async fn board_create(
71 services: &McpServices,
72 principal: &Principal,
73 args: args::IssueBoardCreateArgs,
74) -> CallToolResult {
75 if !principal.can_write() {
76 return write_denied();
77 }
78 let cmd = CreateBoardCmd {
79 name: args.name,
80 parent_dir_id: args.parent_dir_id,
81 icon: args.icon,
82 description: args.description,
83 };
84 match services
85 .issues
86 .create_board(principal.uid, args.space.trim(), cmd)
87 .await
88 {
89 Ok(result) => tool_ok(&result),
90 Err(error) => tool_error(error),
91 }
92}
93
94pub(crate) async fn board_update(
95 services: &McpServices,
96 principal: &Principal,
97 args: args::IssueBoardUpdateArgs,
98) -> CallToolResult {
99 if !principal.can_write() {
100 return write_denied();
101 }
102 let cmd = UpdateBoardCmd {
103 name: args.name,
104 icon: args.icon.map(Some),
105 description: args.description.map(Some),
106 };
107 match services
108 .issues
109 .update_board(principal.uid, args.space.trim(), args.board_id, cmd)
110 .await
111 {
112 Ok(result) => tool_ok(&result),
113 Err(error) => tool_error(error),
114 }
115}
116
117pub(crate) async fn board_delete(
118 services: &McpServices,
119 principal: &Principal,
120 args: args::IssueBoardRefArgs,
121) -> CallToolResult {
122 if !principal.can_write() {
123 return write_denied();
124 }
125 match services
126 .issues
127 .delete_board(principal.uid, args.space.trim(), args.board_id)
128 .await
129 {
130 Ok(()) => tool_done(),
131 Err(error) => tool_error(error),
132 }
133}
134
135pub(crate) async fn list(
136 services: &McpServices,
137 principal: &Principal,
138 args: args::IssueListArgs,
139) -> CallToolResult {
140 let state = match args.state.as_deref() {
141 Some(raw) => match parse_enum::<IssueState>(raw) {
142 Ok(state) => Some(state),
143 Err(error) => return error,
144 },
145 None => None,
146 };
147 let priority = match args.priority.as_deref() {
148 Some(raw) => match parse_enum::<IssuePriority>(raw) {
149 Ok(priority) => Some(priority),
150 Err(error) => return error,
151 },
152 None => None,
153 };
154 let query = ListIssueQuery {
155 state,
156 author_id: args.author_id,
157 milestone_id: args.milestone_id,
158 label_id: args.label_id,
159 assignee_id: args.assignee_id,
160 priority,
161 sort: args.sort,
162 q: args.q,
163 limit: args.limit,
164 offset: args.offset,
165 };
166 match services
167 .issues
168 .list_issues(principal.uid, args.space.trim(), args.board_id, query)
169 .await
170 {
171 Ok(result) => tool_ok(&result),
172 Err(error) => tool_error(error),
173 }
174}
175
176pub(crate) async fn get(
177 services: &McpServices,
178 principal: &Principal,
179 args: args::IssueRefArgs,
180) -> CallToolResult {
181 let issue_id = match resolve_issue(
182 services,
183 principal,
184 &args.space,
185 args.board_id,
186 &args.issue_id,
187 )
188 .await
189 {
190 Ok(id) => id,
191 Err(error) => return error,
192 };
193 match services
194 .issues
195 .get_issue(principal.uid, args.space.trim(), args.board_id, issue_id)
196 .await
197 {
198 Ok(result) => tool_ok(&result),
199 Err(error) => tool_error(error),
200 }
201}
202
203pub(crate) async fn create(
204 services: &McpServices,
205 principal: &Principal,
206 args: args::IssueCreateArgs,
207) -> CallToolResult {
208 if !principal.can_write() {
209 return write_denied();
210 }
211 let priority = match args.priority.as_deref() {
212 Some(raw) => match parse_enum::<IssuePriority>(raw) {
213 Ok(priority) => priority,
214 Err(error) => return error,
215 },
216 None => IssuePriority::None,
217 };
218 let due_at = match args.due_at.as_deref() {
219 Some(raw) => match parse_time(raw) {
220 Ok(time) => Some(time),
221 Err(error) => return error,
222 },
223 None => None,
224 };
225 let cmd = CreateIssueCmd {
226 title: args.title,
227 description: args.description,
228 content: args.content.unwrap_or_default(),
229 milestone_id: args.milestone_id,
230 priority,
231 due_at,
232 assignee_ids: args.assignee_ids,
233 label_ids: args.label_ids,
234 };
235 match services
236 .issues
237 .create_issue(principal.uid, args.space.trim(), args.board_id, cmd)
238 .await
239 {
240 Ok(result) => tool_ok(&result),
241 Err(error) => tool_error(error),
242 }
243}
244
245pub(crate) async fn update(
246 services: &McpServices,
247 principal: &Principal,
248 args: args::IssueUpdateArgs,
249) -> CallToolResult {
250 if !principal.can_write() {
251 return write_denied();
252 }
253 let issue_id = match resolve_issue(
254 services,
255 principal,
256 &args.space,
257 args.board_id,
258 &args.issue_id,
259 )
260 .await
261 {
262 Ok(id) => id,
263 Err(error) => return error,
264 };
265 let priority = match args.priority.as_deref() {
266 Some(raw) => match parse_enum::<IssuePriority>(raw) {
267 Ok(priority) => Some(priority),
268 Err(error) => return error,
269 },
270 None => None,
271 };
272 let due_at = match args.due_at.as_deref() {
273 Some(raw) => match parse_time(raw) {
274 Ok(time) => Some(Some(time)),
275 Err(error) => return error,
276 },
277 None => None,
278 };
279 let cmd = UpdateIssueCmd {
280 title: args.title,
281 description: args.description.map(Some),
282 content: args.content,
283 milestone_id: args.milestone_id.map(Some),
284 priority,
285 due_at,
286 assignee_ids: args.assignee_ids,
287 label_ids: args.label_ids,
288 };
289 match services
290 .issues
291 .update_issue(
292 principal.uid,
293 args.space.trim(),
294 args.board_id,
295 issue_id,
296 cmd,
297 )
298 .await
299 {
300 Ok(result) => tool_ok(&result),
301 Err(error) => tool_error(error),
302 }
303}
304
305pub(crate) async fn delete(
306 services: &McpServices,
307 principal: &Principal,
308 args: args::IssueRefArgs,
309) -> CallToolResult {
310 if !principal.can_write() {
311 return write_denied();
312 }
313 let issue_id = match resolve_issue(
314 services,
315 principal,
316 &args.space,
317 args.board_id,
318 &args.issue_id,
319 )
320 .await
321 {
322 Ok(id) => id,
323 Err(error) => return error,
324 };
325 match services
326 .issues
327 .delete_issue(principal.uid, args.space.trim(), args.board_id, issue_id)
328 .await
329 {
330 Ok(()) => tool_done(),
331 Err(error) => tool_error(error),
332 }
333}
334
335pub(crate) async fn set_closed(
336 services: &McpServices,
337 principal: &Principal,
338 args: args::IssueRefArgs,
339 closed: bool,
340) -> CallToolResult {
341 if !principal.can_write() {
342 return write_denied();
343 }
344 let issue_id = match resolve_issue(
345 services,
346 principal,
347 &args.space,
348 args.board_id,
349 &args.issue_id,
350 )
351 .await
352 {
353 Ok(id) => id,
354 Err(error) => return error,
355 };
356 match services
357 .issues
358 .close_issue(
359 principal.uid,
360 args.space.trim(),
361 args.board_id,
362 issue_id,
363 closed,
364 )
365 .await
366 {
367 Ok(result) => tool_ok(&result),
368 Err(error) => tool_error(error),
369 }
370}
371
372pub(crate) async fn comment_list(
373 services: &McpServices,
374 principal: &Principal,
375 args: args::IssueCommentListArgs,
376) -> CallToolResult {
377 let issue_id = match resolve_issue(
378 services,
379 principal,
380 &args.space,
381 args.board_id,
382 &args.issue_id,
383 )
384 .await
385 {
386 Ok(id) => id,
387 Err(error) => return error,
388 };
389 let query = ListCommentQuery {
390 parent_id: args.parent_id,
391 limit: args.limit,
392 offset: args.offset,
393 sort: args.sort,
394 };
395 match services
396 .issues
397 .list_comments(
398 principal.uid,
399 args.space.trim(),
400 args.board_id,
401 issue_id,
402 query,
403 )
404 .await
405 {
406 Ok(result) => tool_ok(&result),
407 Err(error) => tool_error(error),
408 }
409}
410
411pub(crate) async fn comment_create(
412 services: &McpServices,
413 principal: &Principal,
414 args: args::IssueCommentCreateArgs,
415) -> CallToolResult {
416 if !principal.can_write() {
417 return write_denied();
418 }
419 let issue_id = match resolve_issue(
420 services,
421 principal,
422 &args.space,
423 args.board_id,
424 &args.issue_id,
425 )
426 .await
427 {
428 Ok(id) => id,
429 Err(error) => return error,
430 };
431 let cmd = CreateCommentCmd {
432 content: args.content,
433 parent_id: args.parent_id,
434 };
435 match services
436 .issues
437 .create_comment(
438 principal.uid,
439 args.space.trim(),
440 args.board_id,
441 issue_id,
442 cmd,
443 )
444 .await
445 {
446 Ok(result) => tool_ok(&result),
447 Err(error) => tool_error(error),
448 }
449}
450
451pub(crate) async fn comment_update(
452 services: &McpServices,
453 principal: &Principal,
454 args: args::IssueCommentUpdateArgs,
455) -> CallToolResult {
456 if !principal.can_write() {
457 return write_denied();
458 }
459 let issue_id = match resolve_issue(
460 services,
461 principal,
462 &args.space,
463 args.board_id,
464 &args.issue_id,
465 )
466 .await
467 {
468 Ok(id) => id,
469 Err(error) => return error,
470 };
471 let cmd = UpdateCommentCmd {
472 content: args.content,
473 };
474 match services
475 .issues
476 .update_comment(
477 principal.uid,
478 args.space.trim(),
479 args.board_id,
480 issue_id,
481 args.comment_id,
482 cmd,
483 )
484 .await
485 {
486 Ok(result) => tool_ok(&result),
487 Err(error) => tool_error(error),
488 }
489}
490
491pub(crate) async fn comment_delete(
492 services: &McpServices,
493 principal: &Principal,
494 args: args::IssueCommentRefArgs,
495) -> CallToolResult {
496 if !principal.can_write() {
497 return write_denied();
498 }
499 let issue_id = match resolve_issue(
500 services,
501 principal,
502 &args.space,
503 args.board_id,
504 &args.issue_id,
505 )
506 .await
507 {
508 Ok(id) => id,
509 Err(error) => return error,
510 };
511 match services
512 .issues
513 .delete_comment(
514 principal.uid,
515 args.space.trim(),
516 args.board_id,
517 issue_id,
518 args.comment_id,
519 )
520 .await
521 {
522 Ok(()) => tool_done(),
523 Err(error) => tool_error(error),
524 }
525}
526
527pub(crate) async fn label_list(
528 services: &McpServices,
529 principal: &Principal,
530 args: args::IssueLabelListArgs,
531) -> CallToolResult {
532 let query = ListLabelQuery {
533 q: args.q,
534 is_archived: Some(false),
535 limit: args.limit,
536 offset: args.offset,
537 };
538 match services
539 .issues
540 .list_labels(principal.uid, args.space.trim(), args.board_id, query)
541 .await
542 {
543 Ok(result) => tool_ok(&result),
544 Err(error) => tool_error(error),
545 }
546}
547
548pub(crate) async fn label_create(
549 services: &McpServices,
550 principal: &Principal,
551 args: args::IssueLabelCreateArgs,
552) -> CallToolResult {
553 if !principal.can_write() {
554 return write_denied();
555 }
556 let cmd = CreateLabelCmd {
557 name: args.name,
558 color: args.color,
559 exclusive: args.exclusive,
560 exclusive_order: None,
561 description: args.description,
562 };
563 match services
564 .issues
565 .create_label(principal.uid, args.space.trim(), args.board_id, cmd)
566 .await
567 {
568 Ok(result) => tool_ok(&result),
569 Err(error) => tool_error(error),
570 }
571}
572
573pub(crate) async fn label_update(
574 services: &McpServices,
575 principal: &Principal,
576 args: args::IssueLabelUpdateArgs,
577) -> CallToolResult {
578 if !principal.can_write() {
579 return write_denied();
580 }
581 let cmd = UpdateLabelCmd {
582 name: args.name,
583 color: args.color,
584 exclusive: args.exclusive,
585 exclusive_order: None,
586 description: args.description.map(Some),
587 is_archived: args.is_archived,
588 };
589 match services
590 .issues
591 .update_label(
592 principal.uid,
593 args.space.trim(),
594 args.board_id,
595 args.label_id,
596 cmd,
597 )
598 .await
599 {
600 Ok(result) => tool_ok(&result),
601 Err(error) => tool_error(error),
602 }
603}
604
605pub(crate) async fn label_delete(
606 services: &McpServices,
607 principal: &Principal,
608 args: args::IssueLabelRefArgs,
609) -> CallToolResult {
610 if !principal.can_write() {
611 return write_denied();
612 }
613 match services
614 .issues
615 .delete_label(
616 principal.uid,
617 args.space.trim(),
618 args.board_id,
619 args.label_id,
620 )
621 .await
622 {
623 Ok(()) => tool_done(),
624 Err(error) => tool_error(error),
625 }
626}
627
628pub(crate) async fn label_bind(
629 services: &McpServices,
630 principal: &Principal,
631 args: args::IssueLabelBindArgs,
632) -> CallToolResult {
633 if !principal.can_write() {
634 return write_denied();
635 }
636 let issue_id = match resolve_issue(
637 services,
638 principal,
639 &args.space,
640 args.board_id,
641 &args.issue_id,
642 )
643 .await
644 {
645 Ok(id) => id,
646 Err(error) => return error,
647 };
648 match services
649 .issues
650 .bind_label(
651 principal.uid,
652 args.space.trim(),
653 args.board_id,
654 issue_id,
655 args.label_id,
656 )
657 .await
658 {
659 Ok(()) => tool_done(),
660 Err(error) => tool_error(error),
661 }
662}
663
664pub(crate) async fn label_unbind(
665 services: &McpServices,
666 principal: &Principal,
667 args: args::IssueLabelBindArgs,
668) -> CallToolResult {
669 if !principal.can_write() {
670 return write_denied();
671 }
672 let issue_id = match resolve_issue(
673 services,
674 principal,
675 &args.space,
676 args.board_id,
677 &args.issue_id,
678 )
679 .await
680 {
681 Ok(id) => id,
682 Err(error) => return error,
683 };
684 match services
685 .issues
686 .unbind_label(
687 principal.uid,
688 args.space.trim(),
689 args.board_id,
690 issue_id,
691 args.label_id,
692 )
693 .await
694 {
695 Ok(()) => tool_done(),
696 Err(error) => tool_error(error),
697 }
698}
699
700pub(crate) async fn milestone_list(
701 services: &McpServices,
702 principal: &Principal,
703 args: args::IssueMilestoneListArgs,
704) -> CallToolResult {
705 let query = ListMilestoneQuery {
706 is_closed: args.is_closed,
707 q: args.q,
708 limit: args.limit,
709 offset: args.offset,
710 };
711 match services
712 .issues
713 .list_milestones(principal.uid, args.space.trim(), args.board_id, query)
714 .await
715 {
716 Ok(result) => tool_ok(&result),
717 Err(error) => tool_error(error),
718 }
719}
720
721pub(crate) async fn milestone_create(
722 services: &McpServices,
723 principal: &Principal,
724 args: args::IssueMilestoneCreateArgs,
725) -> CallToolResult {
726 if !principal.can_write() {
727 return write_denied();
728 }
729 let due_at = match args.due_at.as_deref() {
730 Some(raw) => match parse_time(raw) {
731 Ok(time) => Some(time),
732 Err(error) => return error,
733 },
734 None => None,
735 };
736 let cmd = CreateMilestoneCmd {
737 name: args.name,
738 description: args.description,
739 due_at,
740 };
741 match services
742 .issues
743 .create_milestone(principal.uid, args.space.trim(), args.board_id, cmd)
744 .await
745 {
746 Ok(result) => tool_ok(&result),
747 Err(error) => tool_error(error),
748 }
749}
750
751pub(crate) async fn milestone_update(
752 services: &McpServices,
753 principal: &Principal,
754 args: args::IssueMilestoneUpdateArgs,
755) -> CallToolResult {
756 if !principal.can_write() {
757 return write_denied();
758 }
759 let due_at = match args.due_at.as_deref() {
760 Some(raw) => match parse_time(raw) {
761 Ok(time) => Some(Some(time)),
762 Err(error) => return error,
763 },
764 None => None,
765 };
766 let cmd = UpdateMilestoneCmd {
767 name: args.name,
768 description: args.description.map(Some),
769 due_at,
770 is_closed: args.is_closed,
771 };
772 match services
773 .issues
774 .update_milestone(
775 principal.uid,
776 args.space.trim(),
777 args.board_id,
778 args.milestone_id,
779 cmd,
780 )
781 .await
782 {
783 Ok(result) => tool_ok(&result),
784 Err(error) => tool_error(error),
785 }
786}
787
788pub(crate) async fn milestone_delete(
789 services: &McpServices,
790 principal: &Principal,
791 args: args::IssueMilestoneRefArgs,
792) -> CallToolResult {
793 if !principal.can_write() {
794 return write_denied();
795 }
796 match services
797 .issues
798 .delete_milestone(
799 principal.uid,
800 args.space.trim(),
801 args.board_id,
802 args.milestone_id,
803 )
804 .await
805 {
806 Ok(()) => tool_done(),
807 Err(error) => tool_error(error),
808 }
809}
810
811pub(crate) async fn member_list(
812 services: &McpServices,
813 principal: &Principal,
814 args: args::IssueMemberListArgs,
815) -> CallToolResult {
816 let issue_id = match resolve_issue(
817 services,
818 principal,
819 &args.space,
820 args.board_id,
821 &args.issue_id,
822 )
823 .await
824 {
825 Ok(id) => id,
826 Err(error) => return error,
827 };
828 let query = ListIssueMemberQuery {
829 kind: None,
830 limit: args.limit,
831 offset: args.offset,
832 };
833 match services
834 .issues
835 .list_members(
836 principal.uid,
837 args.space.trim(),
838 args.board_id,
839 issue_id,
840 query,
841 )
842 .await
843 {
844 Ok(result) => tool_ok(&result),
845 Err(error) => tool_error(error),
846 }
847}
848
849pub(crate) async fn member_create(
850 services: &McpServices,
851 principal: &Principal,
852 args: args::IssueMemberCreateArgs,
853) -> CallToolResult {
854 if !principal.can_write() {
855 return write_denied();
856 }
857 let issue_id = match resolve_issue(
858 services,
859 principal,
860 &args.space,
861 args.board_id,
862 &args.issue_id,
863 )
864 .await
865 {
866 Ok(id) => id,
867 Err(error) => return error,
868 };
869 let kind = match parse_enum::<IssueMemberKind>(&args.kind) {
870 Ok(kind) => kind,
871 Err(error) => return error,
872 };
873 let cmd = CreateIssueMemberCmd {
874 user_id: args.user_id,
875 kind,
876 };
877 match services
878 .issues
879 .create_member(
880 principal.uid,
881 args.space.trim(),
882 args.board_id,
883 issue_id,
884 cmd,
885 )
886 .await
887 {
888 Ok(result) => tool_ok(&result),
889 Err(error) => tool_error(error),
890 }
891}
892
893pub(crate) async fn member_delete(
894 services: &McpServices,
895 principal: &Principal,
896 args: args::IssueMemberRefArgs,
897) -> CallToolResult {
898 if !principal.can_write() {
899 return write_denied();
900 }
901 let issue_id = match resolve_issue(
902 services,
903 principal,
904 &args.space,
905 args.board_id,
906 &args.issue_id,
907 )
908 .await
909 {
910 Ok(id) => id,
911 Err(error) => return error,
912 };
913 match services
914 .issues
915 .delete_member(
916 principal.uid,
917 args.space.trim(),
918 args.board_id,
919 issue_id,
920 args.member_id,
921 )
922 .await
923 {
924 Ok(()) => tool_done(),
925 Err(error) => tool_error(error),
926 }
927}
928
929pub(crate) async fn pin_list(
930 services: &McpServices,
931 principal: &Principal,
932 args: args::IssuePinListArgs,
933) -> CallToolResult {
934 match services
935 .issues
936 .list_pins(principal.uid, args.space.trim(), args.board_id)
937 .await
938 {
939 Ok(result) => tool_ok(&result),
940 Err(error) => tool_error(error),
941 }
942}
943
944pub(crate) async fn pin_create(
945 services: &McpServices,
946 principal: &Principal,
947 args: args::IssuePinCreateArgs,
948) -> CallToolResult {
949 if !principal.can_write() {
950 return write_denied();
951 }
952 let issue_id = match resolve_issue(
953 services,
954 principal,
955 &args.space,
956 args.board_id,
957 &args.issue_id,
958 )
959 .await
960 {
961 Ok(id) => id,
962 Err(error) => return error,
963 };
964 let cmd = CreatePinCmd {
965 issue_id,
966 pin_order: args.pin_order.unwrap_or(0),
967 };
968 match services
969 .issues
970 .create_pin(principal.uid, args.space.trim(), args.board_id, cmd)
971 .await
972 {
973 Ok(result) => tool_ok(&result),
974 Err(error) => tool_error(error),
975 }
976}
977
978pub(crate) async fn pin_update(
979 services: &McpServices,
980 principal: &Principal,
981 args: args::IssuePinUpdateArgs,
982) -> CallToolResult {
983 if !principal.can_write() {
984 return write_denied();
985 }
986 let cmd = UpdatePinCmd {
987 pin_order: args.pin_order,
988 };
989 match services
990 .issues
991 .update_pin(
992 principal.uid,
993 args.space.trim(),
994 args.board_id,
995 args.pin_id,
996 cmd,
997 )
998 .await
999 {
1000 Ok(result) => tool_ok(&result),
1001 Err(error) => tool_error(error),
1002 }
1003}
1004
1005pub(crate) async fn pin_delete(
1006 services: &McpServices,
1007 principal: &Principal,
1008 args: args::IssuePinRefArgs,
1009) -> CallToolResult {
1010 if !principal.can_write() {
1011 return write_denied();
1012 }
1013 match services
1014 .issues
1015 .delete_pin(principal.uid, args.space.trim(), args.board_id, args.pin_id)
1016 .await
1017 {
1018 Ok(()) => tool_done(),
1019 Err(error) => tool_error(error),
1020 }
1021}
1022
1023pub(crate) async fn dependency_list(
1024 services: &McpServices,
1025 principal: &Principal,
1026 args: args::IssueDependencyListArgs,
1027) -> CallToolResult {
1028 let issue_id = match resolve_issue(
1029 services,
1030 principal,
1031 &args.space,
1032 args.board_id,
1033 &args.issue_id,
1034 )
1035 .await
1036 {
1037 Ok(id) => id,
1038 Err(error) => return error,
1039 };
1040 match services
1041 .issues
1042 .list_dependencies(principal.uid, args.space.trim(), args.board_id, issue_id)
1043 .await
1044 {
1045 Ok(result) => tool_ok(&result),
1046 Err(error) => tool_error(error),
1047 }
1048}
1049
1050pub(crate) async fn dependency_create(
1051 services: &McpServices,
1052 principal: &Principal,
1053 args: args::IssueDependencyCreateArgs,
1054) -> CallToolResult {
1055 if !principal.can_write() {
1056 return write_denied();
1057 }
1058 let issue_id = match resolve_issue(
1059 services,
1060 principal,
1061 &args.space,
1062 args.board_id,
1063 &args.issue_id,
1064 )
1065 .await
1066 {
1067 Ok(id) => id,
1068 Err(error) => return error,
1069 };
1070 let cmd = CreateDependencyCmd {
1071 dependency_id: args.dependency_id,
1072 };
1073 match services
1074 .issues
1075 .create_dependency(
1076 principal.uid,
1077 args.space.trim(),
1078 args.board_id,
1079 issue_id,
1080 cmd,
1081 )
1082 .await
1083 {
1084 Ok(result) => tool_ok(&result),
1085 Err(error) => tool_error(error),
1086 }
1087}
1088
1089pub(crate) async fn dependency_delete(
1090 services: &McpServices,
1091 principal: &Principal,
1092 args: args::IssueDependencyRefArgs,
1093) -> CallToolResult {
1094 if !principal.can_write() {
1095 return write_denied();
1096 }
1097 let issue_id = match resolve_issue(
1098 services,
1099 principal,
1100 &args.space,
1101 args.board_id,
1102 &args.issue_id,
1103 )
1104 .await
1105 {
1106 Ok(id) => id,
1107 Err(error) => return error,
1108 };
1109 match services
1110 .issues
1111 .delete_dependency(
1112 principal.uid,
1113 args.space.trim(),
1114 args.board_id,
1115 issue_id,
1116 args.dep_id,
1117 )
1118 .await
1119 {
1120 Ok(()) => tool_done(),
1121 Err(error) => tool_error(error),
1122 }
1123}
Added lib/mcp-api/src/tools/me.rs View lib/mcp-api/src/tools/me.rs
@@ -0,0 +1,132 @@
1use rmcp::model::CallToolResult;
2use serde_json::json;
3
4use crate::auth::Principal;
5use crate::error::{tool_error, tool_ok};
6use crate::services::McpServices;
7use crate::tools::args;
8
9pub(crate) async fn get(services: &McpServices, principal: &Principal) -> CallToolResult {
10 match services.auth.me(principal.uid).await {
11 Ok((me, profile)) => tool_ok(&json!({
12 "user": me,
13 "profile": profile,
14 "key": {
15 "scope": principal.scope,
16 "full_access": principal.full_access,
17 "source": match principal.source {
18 crate::auth::PrincipalSource::UserKey => "user_key",
19 crate::auth::PrincipalSource::InternalKey => "internal_key",
20 },
21 }
22 })),
23 Err(error) => tool_error(error),
24 }
25}
26
27pub(crate) async fn space_permissions(
28 services: &McpServices,
29 principal: &Principal,
30 args: args::SpaceNameArgs,
31) -> CallToolResult {
32 let space = match space::share::lookup::lookup_space_by_name(&services.space, args.space.trim())
33 .await
34 {
35 Ok(space) => space,
36 Err(error) => return tool_error(error),
37 };
38 match space::share::authz::effective_permissions(
39 services.state.database.read(),
40 &space,
41 principal.uid,
42 )
43 .await
44 {
45 Ok(permissions) => tool_ok(&json!({
46 "space": space.lower_name,
47 "space_id": space.id,
48 "permissions": permissions.into_vec(),
49 })),
50 Err(error) => tool_error(error),
51 }
52}
53
54pub(crate) async fn repo_permissions(
55 services: &McpServices,
56 principal: &Principal,
57 args: args::RepoRefArgs,
58) -> CallToolResult {
59 let ctx = match services
60 .git
61 .context(args.space.trim().to_string(), args.repo.trim().to_string())
62 .await
63 {
64 Ok(ctx) => ctx,
65 Err(error) => return tool_error(error),
66 };
67 let read =
68 git::share::access::authorize_repo_access(&services.git, principal.uid, &ctx, false, false)
69 .await
70 .is_ok();
71 let write =
72 git::share::access::authorize_repo_access(&services.git, principal.uid, &ctx, true, false)
73 .await
74 .is_ok();
75 let admin =
76 git::share::access::authorize_repo_access(&services.git, principal.uid, &ctx, true, true)
77 .await
78 .is_ok();
79 tool_ok(&json!({
80 "space": args.space.trim(),
81 "repo": ctx.model.name,
82 "repo_id": ctx.model.id,
83 "is_private": ctx.model.is_private,
84 "read": read,
85 "write": write,
86 "admin": admin,
87 }))
88}
89
90pub(crate) async fn user_get(
91 services: &McpServices,
92 principal: &Principal,
93 args: args::UserRefArgs,
94) -> CallToolResult {
95 let result = if let Some(uid) = args.user_id {
96 services.auth.public_user(principal.uid, uid).await
97 } else if let Some(username) = args.username.as_deref() {
98 services
99 .auth
100 .public_user_by_name(principal.uid, username)
101 .await
102 } else {
103 return crate::error::tool_message("user_id 与 username 至少提供一个");
104 };
105 match result {
106 Ok(user) => tool_ok(&user),
107 Err(error) => tool_error(error),
108 }
109}
110
111pub(crate) async fn users_get(
112 services: &McpServices,
113 principal: &Principal,
114 args: args::UserIdsArgs,
115) -> CallToolResult {
116 match services.auth.public_users(principal.uid, args.ids).await {
117 Ok(users) => tool_ok(&users),
118 Err(error) => tool_error(error),
119 }
120}
121
122pub(crate) async fn my_spaces(services: &McpServices, principal: &Principal) -> CallToolResult {
123 let query = export::pagination::PageQuery {
124 limit: None,
125 offset: None,
126 sort: None,
127 };
128 match services.user.my_spaces(principal.uid, query).await {
129 Ok(spaces) => tool_ok(&spaces),
130 Err(error) => tool_error(error),
131 }
132}
Added lib/mcp-api/src/tools/mod.rs View lib/mcp-api/src/tools/mod.rs
@@ -0,0 +1,1470 @@
1pub(crate) mod args;
2pub(crate) mod helpers;
3pub(crate) mod issue;
4pub(crate) mod me;
5pub(crate) mod pull;
6pub(crate) mod repo;
7pub(crate) mod space;
8
9use rmcp::ErrorData;
10use rmcp::handler::server::wrapper::Parameters;
11use rmcp::model::CallToolResult;
12use rmcp::service::RequestContext;
13use rmcp::{RoleServer, tool, tool_router};
14
15use crate::McpServer;
16
17#[tool_router(vis = "pub(crate)")]
18impl McpServer {
19 #[tool(
20 description = "返回当前访问密钥对应的用户身份,以及密钥的读写范围(scope)。",
21 annotations(read_only_hint = true)
22 )]
23 async fn me_get(&self, ctx: RequestContext<RoleServer>) -> Result<CallToolResult, ErrorData> {
24 let principal = self.principal(&ctx)?;
25 Ok(me::get(&self.services, &principal).await)
26 }
27
28 #[tool(
29 description = "查询当前访问密钥的用户在指定 space 中的全部有效权限(view_space/read/message/manage_*)。",
30 annotations(read_only_hint = true)
31 )]
32 async fn me_space_permissions(
33 &self,
34 Parameters(args): Parameters<args::SpaceNameArgs>,
35 ctx: RequestContext<RoleServer>,
36 ) -> Result<CallToolResult, ErrorData> {
37 let principal = self.principal(&ctx)?;
38 Ok(me::space_permissions(&self.services, &principal, args).await)
39 }
40
41 #[tool(
42 description = "查询当前访问密钥的用户在指定仓库上的 read/write/admin 权限。",
43 annotations(read_only_hint = true)
44 )]
45 async fn me_repo_permissions(
46 &self,
47 Parameters(args): Parameters<args::RepoRefArgs>,
48 ctx: RequestContext<RoleServer>,
49 ) -> Result<CallToolResult, ErrorData> {
50 let principal = self.principal(&ctx)?;
51 Ok(me::repo_permissions(&self.services, &principal, args).await)
52 }
53
54 #[tool(
55 description = "返回当前访问密钥的用户拥有的 space 列表。",
56 annotations(read_only_hint = true)
57 )]
58 async fn me_spaces(
59 &self,
60 ctx: RequestContext<RoleServer>,
61 ) -> Result<CallToolResult, ErrorData> {
62 let principal = self.principal(&ctx)?;
63 Ok(me::my_spaces(&self.services, &principal).await)
64 }
65
66 #[tool(
67 description = "按用户名或 UID 查询平台用户的公开资料。",
68 annotations(read_only_hint = true)
69 )]
70 async fn user_get(
71 &self,
72 Parameters(args): Parameters<args::UserRefArgs>,
73 ctx: RequestContext<RoleServer>,
74 ) -> Result<CallToolResult, ErrorData> {
75 let principal = self.principal(&ctx)?;
76 Ok(me::user_get(&self.services, &principal, args).await)
77 }
78
79 #[tool(
80 description = "按一组 UID 批量查询平台用户公开资料(最多 100 个)。",
81 annotations(read_only_hint = true)
82 )]
83 async fn users_get(
84 &self,
85 Parameters(args): Parameters<args::UserIdsArgs>,
86 ctx: RequestContext<RoleServer>,
87 ) -> Result<CallToolResult, ErrorData> {
88 let principal = self.principal(&ctx)?;
89 Ok(me::users_get(&self.services, &principal, args).await)
90 }
91
92 #[tool(
93 description = "列出当前用户拥有的 space。",
94 annotations(read_only_hint = true)
95 )]
96 async fn space_list(
97 &self,
98 Parameters(args): Parameters<args::SpaceListArgs>,
99 ctx: RequestContext<RoleServer>,
100 ) -> Result<CallToolResult, ErrorData> {
101 let principal = self.principal(&ctx)?;
102 Ok(space::list(&self.services, &principal, args).await)
103 }
104
105 #[tool(
106 description = "浏览公开 space(未私有的可加入空间)。",
107 annotations(read_only_hint = true)
108 )]
109 async fn space_explore(
110 &self,
111 Parameters(args): Parameters<args::SpaceExploreArgs>,
112 ctx: RequestContext<RoleServer>,
113 ) -> Result<CallToolResult, ErrorData> {
114 let principal = self.principal(&ctx)?;
115 Ok(space::explore(&self.services, &principal, args).await)
116 }
117
118 #[tool(
119 description = "按名称获取 space 详情(私有时需要成员权限)。",
120 annotations(read_only_hint = true)
121 )]
122 async fn space_get(
123 &self,
124 Parameters(args): Parameters<args::SpaceNameArgs>,
125 ctx: RequestContext<RoleServer>,
126 ) -> Result<CallToolResult, ErrorData> {
127 let principal = self.principal(&ctx)?;
128 Ok(space::get(&self.services, &principal, args).await)
129 }
130
131 #[tool(description = "创建 space(当前用户成为所有者)。")]
132 async fn space_create(
133 &self,
134 Parameters(args): Parameters<args::SpaceCreateArgs>,
135 ctx: RequestContext<RoleServer>,
136 ) -> Result<CallToolResult, ErrorData> {
137 let principal = self.principal(&ctx)?;
138 Ok(space::create(&self.services, &principal, args).await)
139 }
140
141 #[tool(description = "更新 space 设置(仅所有者)。")]
142 async fn space_update(
143 &self,
144 Parameters(args): Parameters<args::SpaceUpdateArgs>,
145 ctx: RequestContext<RoleServer>,
146 ) -> Result<CallToolResult, ErrorData> {
147 let principal = self.principal(&ctx)?;
148 Ok(space::update(&self.services, &principal, args).await)
149 }
150
151 #[tool(
152 description = "停用(软删除)space(仅所有者)。",
153 annotations(destructive_hint = true)
154 )]
155 async fn space_delete(
156 &self,
157 Parameters(args): Parameters<args::SpaceNameArgs>,
158 ctx: RequestContext<RoleServer>,
159 ) -> Result<CallToolResult, ErrorData> {
160 let principal = self.principal(&ctx)?;
161 Ok(space::delete(&self.services, &principal, args).await)
162 }
163
164 #[tool(description = "列出 space 成员。", annotations(read_only_hint = true))]
165 async fn space_member_list(
166 &self,
167 Parameters(args): Parameters<args::SpaceMemberListArgs>,
168 ctx: RequestContext<RoleServer>,
169 ) -> Result<CallToolResult, ErrorData> {
170 let principal = self.principal(&ctx)?;
171 Ok(space::member_list(&self.services, &principal, args).await)
172 }
173
174 #[tool(description = "添加 space 成员(需要 manage_members 权限)。")]
175 async fn space_member_add(
176 &self,
177 Parameters(args): Parameters<args::SpaceMemberAddArgs>,
178 ctx: RequestContext<RoleServer>,
179 ) -> Result<CallToolResult, ErrorData> {
180 let principal = self.principal(&ctx)?;
181 Ok(space::member_add(&self.services, &principal, args).await)
182 }
183
184 #[tool(description = "更新 space 成员属性(需要 manage_members 权限)。")]
185 async fn space_member_update(
186 &self,
187 Parameters(args): Parameters<args::SpaceMemberUpdateArgs>,
188 ctx: RequestContext<RoleServer>,
189 ) -> Result<CallToolResult, ErrorData> {
190 let principal = self.principal(&ctx)?;
191 Ok(space::member_update(&self.services, &principal, args).await)
192 }
193
194 #[tool(
195 description = "移除 space 成员(需要 manage_members 权限)。",
196 annotations(destructive_hint = true)
197 )]
198 async fn space_member_remove(
199 &self,
200 Parameters(args): Parameters<args::SpaceMemberRefArgs>,
201 ctx: RequestContext<RoleServer>,
202 ) -> Result<CallToolResult, ErrorData> {
203 let principal = self.principal(&ctx)?;
204 Ok(space::member_remove(&self.services, &principal, args).await)
205 }
206
207 #[tool(description = "主动退出 space(所有者不可退出)。")]
208 async fn space_member_leave(
209 &self,
210 Parameters(args): Parameters<args::SpaceNameArgs>,
211 ctx: RequestContext<RoleServer>,
212 ) -> Result<CallToolResult, ErrorData> {
213 let principal = self.principal(&ctx)?;
214 Ok(space::member_leave(&self.services, &principal, args).await)
215 }
216
217 #[tool(
218 description = "列出 space 的角色组(需要 manage_roles 权限)。",
219 annotations(read_only_hint = true)
220 )]
221 async fn space_group_list(
222 &self,
223 Parameters(args): Parameters<args::SpaceGroupListArgs>,
224 ctx: RequestContext<RoleServer>,
225 ) -> Result<CallToolResult, ErrorData> {
226 let principal = self.principal(&ctx)?;
227 Ok(space::group_list(&self.services, &principal, args).await)
228 }
229
230 #[tool(description = "创建 space 角色组(需要 manage_roles 权限)。")]
231 async fn space_group_create(
232 &self,
233 Parameters(args): Parameters<args::SpaceGroupCreateArgs>,
234 ctx: RequestContext<RoleServer>,
235 ) -> Result<CallToolResult, ErrorData> {
236 let principal = self.principal(&ctx)?;
237 Ok(space::group_create(&self.services, &principal, args).await)
238 }
239
240 #[tool(description = "更新 space 角色组(需要 manage_roles 权限)。")]
241 async fn space_group_update(
242 &self,
243 Parameters(args): Parameters<args::SpaceGroupUpdateArgs>,
244 ctx: RequestContext<RoleServer>,
245 ) -> Result<CallToolResult, ErrorData> {
246 let principal = self.principal(&ctx)?;
247 Ok(space::group_update(&self.services, &principal, args).await)
248 }
249
250 #[tool(
251 description = "删除 space 角色组(需要 manage_roles 权限)。",
252 annotations(destructive_hint = true)
253 )]
254 async fn space_group_delete(
255 &self,
256 Parameters(args): Parameters<args::SpaceGroupRefArgs>,
257 ctx: RequestContext<RoleServer>,
258 ) -> Result<CallToolResult, ErrorData> {
259 let principal = self.principal(&ctx)?;
260 Ok(space::group_delete(&self.services, &principal, args).await)
261 }
262
263 #[tool(description = "把成员加入角色组(需要 manage_roles 权限)。")]
264 async fn space_group_assign(
265 &self,
266 Parameters(args): Parameters<args::SpaceGroupAssignArgs>,
267 ctx: RequestContext<RoleServer>,
268 ) -> Result<CallToolResult, ErrorData> {
269 let principal = self.principal(&ctx)?;
270 Ok(space::group_assign(&self.services, &principal, args).await)
271 }
272
273 #[tool(description = "把成员移出角色组(需要 manage_roles 权限)。")]
274 async fn space_group_unassign(
275 &self,
276 Parameters(args): Parameters<args::SpaceGroupAssignArgs>,
277 ctx: RequestContext<RoleServer>,
278 ) -> Result<CallToolResult, ErrorData> {
279 let principal = self.principal(&ctx)?;
280 Ok(space::group_unassign(&self.services, &principal, args).await)
281 }
282
283 #[tool(
284 description = "获取 space 的目录树(可见性受目录覆盖规则限制)。",
285 annotations(read_only_hint = true)
286 )]
287 async fn space_dir_tree(
288 &self,
289 Parameters(args): Parameters<args::SpaceDirTreeArgs>,
290 ctx: RequestContext<RoleServer>,
291 ) -> Result<CallToolResult, ErrorData> {
292 let principal = self.principal(&ctx)?;
293 Ok(space::dir_tree(&self.services, &principal, args).await)
294 }
295
296 #[tool(description = "创建目录节点(需要 manage_channels 权限)。")]
297 async fn space_dir_create(
298 &self,
299 Parameters(args): Parameters<args::SpaceDirCreateArgs>,
300 ctx: RequestContext<RoleServer>,
301 ) -> Result<CallToolResult, ErrorData> {
302 let principal = self.principal(&ctx)?;
303 Ok(space::dir_create(&self.services, &principal, args).await)
304 }
305
306 #[tool(description = "重命名目录节点(需要 manage_channels 权限)。")]
307 async fn space_dir_update(
308 &self,
309 Parameters(args): Parameters<args::SpaceDirUpdateArgs>,
310 ctx: RequestContext<RoleServer>,
311 ) -> Result<CallToolResult, ErrorData> {
312 let principal = self.principal(&ctx)?;
313 Ok(space::dir_update(&self.services, &principal, args).await)
314 }
315
316 #[tool(description = "移动目录节点(需要 manage_channels 权限)。")]
317 async fn space_dir_move(
318 &self,
319 Parameters(args): Parameters<args::SpaceDirMoveArgs>,
320 ctx: RequestContext<RoleServer>,
321 ) -> Result<CallToolResult, ErrorData> {
322 let principal = self.principal(&ctx)?;
323 Ok(space::dir_move(&self.services, &principal, args).await)
324 }
325
326 #[tool(
327 description = "列出 space 邀请(需要 manage_invites 权限)。",
328 annotations(read_only_hint = true)
329 )]
330 async fn space_invite_list(
331 &self,
332 Parameters(args): Parameters<args::SpaceInviteListArgs>,
333 ctx: RequestContext<RoleServer>,
334 ) -> Result<CallToolResult, ErrorData> {
335 let principal = self.principal(&ctx)?;
336 Ok(space::invite_list(&self.services, &principal, args).await)
337 }
338
339 #[tool(description = "创建 space 邀请(需要 manage_invites 权限)。")]
340 async fn space_invite_create(
341 &self,
342 Parameters(args): Parameters<args::SpaceInviteCreateArgs>,
343 ctx: RequestContext<RoleServer>,
344 ) -> Result<CallToolResult, ErrorData> {
345 let principal = self.principal(&ctx)?;
346 Ok(space::invite_create(&self.services, &principal, args).await)
347 }
348
349 #[tool(
350 description = "撤销 space 邀请(需要 manage_invites 权限)。",
351 annotations(destructive_hint = true)
352 )]
353 async fn space_invite_revoke(
354 &self,
355 Parameters(args): Parameters<args::SpaceInviteRefArgs>,
356 ctx: RequestContext<RoleServer>,
357 ) -> Result<CallToolResult, ErrorData> {
358 let principal = self.principal(&ctx)?;
359 Ok(space::invite_revoke(&self.services, &principal, args).await)
360 }
361
362 #[tool(
363 description = "列出 space 加入申请(需要 manage_members 权限)。",
364 annotations(read_only_hint = true)
365 )]
366 async fn space_join_request_list(
367 &self,
368 Parameters(args): Parameters<args::SpaceJoinRequestListArgs>,
369 ctx: RequestContext<RoleServer>,
370 ) -> Result<CallToolResult, ErrorData> {
371 let principal = self.principal(&ctx)?;
372 Ok(space::join_request_list(&self.services, &principal, args).await)
373 }
374
375 #[tool(description = "提交加入 space 的申请。")]
376 async fn space_join_request_create(
377 &self,
378 Parameters(args): Parameters<args::SpaceJoinRequestCreateArgs>,
379 ctx: RequestContext<RoleServer>,
380 ) -> Result<CallToolResult, ErrorData> {
381 let principal = self.principal(&ctx)?;
382 Ok(space::join_request_create(&self.services, &principal, args).await)
383 }
384
385 #[tool(description = "撤回自己的加入申请。")]
386 async fn space_join_request_withdraw(
387 &self,
388 Parameters(args): Parameters<args::SpaceJoinRequestRefArgs>,
389 ctx: RequestContext<RoleServer>,
390 ) -> Result<CallToolResult, ErrorData> {
391 let principal = self.principal(&ctx)?;
392 Ok(space::join_request_withdraw(&self.services, &principal, args).await)
393 }
394
395 #[tool(description = "通过加入申请(需要 manage_members 权限)。")]
396 async fn space_join_request_approve(
397 &self,
398 Parameters(args): Parameters<args::SpaceJoinRequestRefArgs>,
399 ctx: RequestContext<RoleServer>,
400 ) -> Result<CallToolResult, ErrorData> {
401 let principal = self.principal(&ctx)?;
402 Ok(space::join_request_approve(&self.services, &principal, args).await)
403 }
404
405 #[tool(description = "拒绝加入申请(需要 manage_members 权限)。")]
406 async fn space_join_request_reject(
407 &self,
408 Parameters(args): Parameters<args::SpaceJoinRequestRefArgs>,
409 ctx: RequestContext<RoleServer>,
410 ) -> Result<CallToolResult, ErrorData> {
411 let principal = self.principal(&ctx)?;
412 Ok(space::join_request_reject(&self.services, &principal, args).await)
413 }
414
415 #[tool(
416 description = "列出 space 下的仓库(私有仓库仅协作者可见)。",
417 annotations(read_only_hint = true)
418 )]
419 async fn repo_list(
420 &self,
421 Parameters(args): Parameters<args::RepoListArgs>,
422 ctx: RequestContext<RoleServer>,
423 ) -> Result<CallToolResult, ErrorData> {
424 let principal = self.principal(&ctx)?;
425 Ok(repo::list(&self.services, &principal, args).await)
426 }
427
428 #[tool(
429 description = "获取仓库详情(需要读权限)。",
430 annotations(read_only_hint = true)
431 )]
432 async fn repo_get(
433 &self,
434 Parameters(args): Parameters<args::RepoRefArgs>,
435 ctx: RequestContext<RoleServer>,
436 ) -> Result<CallToolResult, ErrorData> {
437 let principal = self.principal(&ctx)?;
438 Ok(repo::get(&self.services, &principal, args).await)
439 }
440
441 #[tool(description = "在 space 下创建仓库(需要 space 所有者权限)。")]
442 async fn repo_create(
443 &self,
444 Parameters(args): Parameters<args::RepoCreateArgs>,
445 ctx: RequestContext<RoleServer>,
446 ) -> Result<CallToolResult, ErrorData> {
447 let principal = self.principal(&ctx)?;
448 Ok(repo::create(&self.services, &principal, args).await)
449 }
450
451 #[tool(description = "更新仓库设置(需要 space 所有者权限)。")]
452 async fn repo_update(
453 &self,
454 Parameters(args): Parameters<args::RepoUpdateArgs>,
455 ctx: RequestContext<RoleServer>,
456 ) -> Result<CallToolResult, ErrorData> {
457 let principal = self.principal(&ctx)?;
458 Ok(repo::update(&self.services, &principal, args).await)
459 }
460
461 #[tool(description = "归档或取消归档仓库(需要 space 所有者权限)。")]
462 async fn repo_set_archived(
463 &self,
464 Parameters(args): Parameters<args::RepoArchiveArgs>,
465 ctx: RequestContext<RoleServer>,
466 ) -> Result<CallToolResult, ErrorData> {
467 let principal = self.principal(&ctx)?;
468 Ok(repo::set_archived(&self.services, &principal, args).await)
469 }
470
471 #[tool(description = "列出仓库分支。", annotations(read_only_hint = true))]
472 async fn repo_branch_list(
473 &self,
474 Parameters(args): Parameters<args::RepoRefArgs>,
475 ctx: RequestContext<RoleServer>,
476 ) -> Result<CallToolResult, ErrorData> {
477 let principal = self.principal(&ctx)?;
478 Ok(repo::branch_list(&self.services, &principal, args).await)
479 }
480
481 #[tool(description = "获取单个分支信息。", annotations(read_only_hint = true))]
482 async fn repo_branch_get(
483 &self,
484 Parameters(args): Parameters<args::RepoBranchGetArgs>,
485 ctx: RequestContext<RoleServer>,
486 ) -> Result<CallToolResult, ErrorData> {
487 let principal = self.principal(&ctx)?;
488 Ok(repo::branch_get(&self.services, &principal, args).await)
489 }
490
491 #[tool(description = "创建分支(需要仓库写权限)。")]
492 async fn repo_branch_create(
493 &self,
494 Parameters(args): Parameters<args::RepoBranchCreateArgs>,
495 ctx: RequestContext<RoleServer>,
496 ) -> Result<CallToolResult, ErrorData> {
497 let principal = self.principal(&ctx)?;
498 Ok(repo::branch_create(&self.services, &principal, args).await)
499 }
500
501 #[tool(description = "重命名分支(需要仓库写权限)。")]
502 async fn repo_branch_rename(
503 &self,
504 Parameters(args): Parameters<args::RepoBranchRenameArgs>,
505 ctx: RequestContext<RoleServer>,
506 ) -> Result<CallToolResult, ErrorData> {
507 let principal = self.principal(&ctx)?;
508 Ok(repo::branch_rename(&self.services, &principal, args).await)
509 }
510
511 #[tool(
512 description = "删除分支(需要仓库写权限)。",
513 annotations(destructive_hint = true)
514 )]
515 async fn repo_branch_delete(
516 &self,
517 Parameters(args): Parameters<args::RepoBranchDeleteArgs>,
518 ctx: RequestContext<RoleServer>,
519 ) -> Result<CallToolResult, ErrorData> {
520 let principal = self.principal(&ctx)?;
521 Ok(repo::branch_delete(&self.services, &principal, args).await)
522 }
523
524 #[tool(description = "设置默认分支(需要仓库写权限)。")]
525 async fn repo_branch_set_default(
526 &self,
527 Parameters(args): Parameters<args::RepoBranchDefaultArgs>,
528 ctx: RequestContext<RoleServer>,
529 ) -> Result<CallToolResult, ErrorData> {
530 let principal = self.principal(&ctx)?;
531 Ok(repo::branch_set_default(&self.services, &principal, args).await)
532 }
533
534 #[tool(description = "列出仓库标签。", annotations(read_only_hint = true))]
535 async fn repo_tag_list(
536 &self,
537 Parameters(args): Parameters<args::RepoRefArgs>,
538 ctx: RequestContext<RoleServer>,
539 ) -> Result<CallToolResult, ErrorData> {
540 let principal = self.principal(&ctx)?;
541 Ok(repo::tag_list(&self.services, &principal, args).await)
542 }
543
544 #[tool(description = "获取单个标签。", annotations(read_only_hint = true))]
545 async fn repo_tag_get(
546 &self,
547 Parameters(args): Parameters<args::RepoTagGetArgs>,
548 ctx: RequestContext<RoleServer>,
549 ) -> Result<CallToolResult, ErrorData> {
550 let principal = self.principal(&ctx)?;
551 Ok(repo::tag_get(&self.services, &principal, args).await)
552 }
553
554 #[tool(description = "创建标签(需要仓库写权限)。")]
555 async fn repo_tag_create(
556 &self,
557 Parameters(args): Parameters<args::RepoTagCreateArgs>,
558 ctx: RequestContext<RoleServer>,
559 ) -> Result<CallToolResult, ErrorData> {
560 let principal = self.principal(&ctx)?;
561 Ok(repo::tag_create(&self.services, &principal, args).await)
562 }
563
564 #[tool(
565 description = "删除标签(需要仓库写权限)。",
566 annotations(destructive_hint = true)
567 )]
568 async fn repo_tag_delete(
569 &self,
570 Parameters(args): Parameters<args::RepoTagDeleteArgs>,
571 ctx: RequestContext<RoleServer>,
572 ) -> Result<CallToolResult, ErrorData> {
573 let principal = self.principal(&ctx)?;
574 Ok(repo::tag_delete(&self.services, &principal, args).await)
575 }
576
577 #[tool(
578 description = "列出提交历史(可按路径/数量过滤)。",
579 annotations(read_only_hint = true)
580 )]
581 async fn repo_commit_list(
582 &self,
583 Parameters(args): Parameters<args::RepoCommitListArgs>,
584 ctx: RequestContext<RoleServer>,
585 ) -> Result<CallToolResult, ErrorData> {
586 let principal = self.principal(&ctx)?;
587 Ok(repo::commit_list(&self.services, &principal, args).await)
588 }
589
590 #[tool(description = "获取单个提交详情。", annotations(read_only_hint = true))]
591 async fn repo_commit_get(
592 &self,
593 Parameters(args): Parameters<args::RepoCommitGetArgs>,
594 ctx: RequestContext<RoleServer>,
595 ) -> Result<CallToolResult, ErrorData> {
596 let principal = self.principal(&ctx)?;
597 Ok(repo::commit_get(&self.services, &principal, args).await)
598 }
599
600 #[tool(
601 description = "浏览仓库目录树(recurse=true 递归列出全部条目)。",
602 annotations(read_only_hint = true)
603 )]
604 async fn repo_tree(
605 &self,
606 Parameters(args): Parameters<args::RepoTreeArgs>,
607 ctx: RequestContext<RoleServer>,
608 ) -> Result<CallToolResult, ErrorData> {
609 let principal = self.principal(&ctx)?;
610 Ok(repo::tree(&self.services, &principal, args).await)
611 }
612
613 #[tool(
614 description = "按路径读取仓库文件内容(文本返回 utf-8,二进制返回 base64)。",
615 annotations(read_only_hint = true)
616 )]
617 async fn repo_file_get(
618 &self,
619 Parameters(args): Parameters<args::RepoFileArgs>,
620 ctx: RequestContext<RoleServer>,
621 ) -> Result<CallToolResult, ErrorData> {
622 let principal = self.principal(&ctx)?;
623 Ok(repo::file_get(&self.services, &principal, args).await)
624 }
625
626 #[tool(description = "列出仓库 Release。", annotations(read_only_hint = true))]
627 async fn repo_release_list(
628 &self,
629 Parameters(args): Parameters<args::RepoReleaseListArgs>,
630 ctx: RequestContext<RoleServer>,
631 ) -> Result<CallToolResult, ErrorData> {
632 let principal = self.principal(&ctx)?;
633 Ok(repo::release_list(&self.services, &principal, args).await)
634 }
635
636 #[tool(
637 description = "按 tag 获取 Release。",
638 annotations(read_only_hint = true)
639 )]
640 async fn repo_release_get(
641 &self,
642 Parameters(args): Parameters<args::RepoReleaseGetArgs>,
643 ctx: RequestContext<RoleServer>,
644 ) -> Result<CallToolResult, ErrorData> {
645 let principal = self.principal(&ctx)?;
646 Ok(repo::release_get(&self.services, &principal, args).await)
647 }
648
649 #[tool(description = "创建 Release(需要仓库写权限)。")]
650 async fn repo_release_create(
651 &self,
652 Parameters(args): Parameters<args::RepoReleaseCreateArgs>,
653 ctx: RequestContext<RoleServer>,
654 ) -> Result<CallToolResult, ErrorData> {
655 let principal = self.principal(&ctx)?;
656 Ok(repo::release_create(&self.services, &principal, args).await)
657 }
658
659 #[tool(description = "更新 Release(发布者或仓库管理员)。")]
660 async fn repo_release_update(
661 &self,
662 Parameters(args): Parameters<args::RepoReleaseUpdateArgs>,
663 ctx: RequestContext<RoleServer>,
664 ) -> Result<CallToolResult, ErrorData> {
665 let principal = self.principal(&ctx)?;
666 Ok(repo::release_update(&self.services, &principal, args).await)
667 }
668
669 #[tool(
670 description = "删除 Release(发布者或仓库管理员)。",
671 annotations(destructive_hint = true)
672 )]
673 async fn repo_release_delete(
674 &self,
675 Parameters(args): Parameters<args::RepoReleaseDeleteArgs>,
676 ctx: RequestContext<RoleServer>,
677 ) -> Result<CallToolResult, ErrorData> {
678 let principal = self.principal(&ctx)?;
679 Ok(repo::release_delete(&self.services, &principal, args).await)
680 }
681
682 #[tool(description = "列出仓库 topics。", annotations(read_only_hint = true))]
683 async fn repo_topic_list(
684 &self,
685 Parameters(args): Parameters<args::RepoTopicArgs>,
686 ctx: RequestContext<RoleServer>,
687 ) -> Result<CallToolResult, ErrorData> {
688 let principal = self.principal(&ctx)?;
689 Ok(repo::topic_list(&self.services, &principal, args).await)
690 }
691
692 #[tool(description = "为仓库添加 topic(需要仓库写权限)。")]
693 async fn repo_topic_add(
694 &self,
695 Parameters(args): Parameters<args::RepoTopicChangeArgs>,
696 ctx: RequestContext<RoleServer>,
697 ) -> Result<CallToolResult, ErrorData> {
698 let principal = self.principal(&ctx)?;
699 Ok(repo::topic_add(&self.services, &principal, args).await)
700 }
701
702 #[tool(description = "移除仓库 topic(需要仓库写权限)。")]
703 async fn repo_topic_remove(
704 &self,
705 Parameters(args): Parameters<args::RepoTopicChangeArgs>,
706 ctx: RequestContext<RoleServer>,
707 ) -> Result<CallToolResult, ErrorData> {
708 let principal = self.principal(&ctx)?;
709 Ok(repo::topic_remove(&self.services, &principal, args).await)
710 }
711
712 #[tool(description = "收藏仓库。")]
713 async fn repo_star(
714 &self,
715 Parameters(args): Parameters<args::RepoRefArgs>,
716 ctx: RequestContext<RoleServer>,
717 ) -> Result<CallToolResult, ErrorData> {
718 let principal = self.principal(&ctx)?;
719 Ok(repo::star(&self.services, &principal, args).await)
720 }
721
722 #[tool(description = "取消收藏仓库。")]
723 async fn repo_unstar(
724 &self,
725 Parameters(args): Parameters<args::RepoRefArgs>,
726 ctx: RequestContext<RoleServer>,
727 ) -> Result<CallToolResult, ErrorData> {
728 let principal = self.principal(&ctx)?;
729 Ok(repo::unstar(&self.services, &principal, args).await)
730 }
731
732 #[tool(description = "设置仓库关注模式(unwatched/watching/ignore)。")]
733 async fn repo_watch(
734 &self,
735 Parameters(args): Parameters<args::RepoWatchArgs>,
736 ctx: RequestContext<RoleServer>,
737 ) -> Result<CallToolResult, ErrorData> {
738 let principal = self.principal(&ctx)?;
739 Ok(repo::watch(&self.services, &principal, args).await)
740 }
741
742 #[tool(description = "列出仓库收藏者。", annotations(read_only_hint = true))]
743 async fn repo_stargazers(
744 &self,
745 Parameters(args): Parameters<args::RepoStargazerListArgs>,
746 ctx: RequestContext<RoleServer>,
747 ) -> Result<CallToolResult, ErrorData> {
748 let principal = self.principal(&ctx)?;
749 Ok(repo::stargazers(&self.services, &principal, args).await)
750 }
751
752 #[tool(description = "列出仓库关注者。", annotations(read_only_hint = true))]
753 async fn repo_watchers(
754 &self,
755 Parameters(args): Parameters<args::RepoWatcherListArgs>,
756 ctx: RequestContext<RoleServer>,
757 ) -> Result<CallToolResult, ErrorData> {
758 let principal = self.principal(&ctx)?;
759 Ok(repo::watchers(&self.services, &principal, args).await)
760 }
761
762 #[tool(
763 description = "列出 space 内的 issue 看板。",
764 annotations(read_only_hint = true)
765 )]
766 async fn issue_board_list(
767 &self,
768 Parameters(args): Parameters<args::IssueBoardListArgs>,
769 ctx: RequestContext<RoleServer>,
770 ) -> Result<CallToolResult, ErrorData> {
771 let principal = self.principal(&ctx)?;
772 Ok(issue::board_list(&self.services, &principal, args).await)
773 }
774
775 #[tool(
776 description = "获取 issue 看板详情。",
777 annotations(read_only_hint = true)
778 )]
779 async fn issue_board_get(
780 &self,
781 Parameters(args): Parameters<args::IssueBoardRefArgs>,
782 ctx: RequestContext<RoleServer>,
783 ) -> Result<CallToolResult, ErrorData> {
784 let principal = self.principal(&ctx)?;
785 Ok(issue::board_get(&self.services, &principal, args).await)
786 }
787
788 #[tool(description = "创建 issue 看板(需要 manage_channels 权限)。")]
789 async fn issue_board_create(
790 &self,
791 Parameters(args): Parameters<args::IssueBoardCreateArgs>,
792 ctx: RequestContext<RoleServer>,
793 ) -> Result<CallToolResult, ErrorData> {
794 let principal = self.principal(&ctx)?;
795 Ok(issue::board_create(&self.services, &principal, args).await)
796 }
797
798 #[tool(description = "更新 issue 看板(需要管理权限)。")]
799 async fn issue_board_update(
800 &self,
801 Parameters(args): Parameters<args::IssueBoardUpdateArgs>,
802 ctx: RequestContext<RoleServer>,
803 ) -> Result<CallToolResult, ErrorData> {
804 let principal = self.principal(&ctx)?;
805 Ok(issue::board_update(&self.services, &principal, args).await)
806 }
807
808 #[tool(
809 description = "删除 issue 看板(需要管理权限)。",
810 annotations(destructive_hint = true)
811 )]
812 async fn issue_board_delete(
813 &self,
814 Parameters(args): Parameters<args::IssueBoardRefArgs>,
815 ctx: RequestContext<RoleServer>,
816 ) -> Result<CallToolResult, ErrorData> {
817 let principal = self.principal(&ctx)?;
818 Ok(issue::board_delete(&self.services, &principal, args).await)
819 }
820
821 #[tool(
822 description = "分页/过滤查询 issue。",
823 annotations(read_only_hint = true)
824 )]
825 async fn issue_list(
826 &self,
827 Parameters(args): Parameters<args::IssueListArgs>,
828 ctx: RequestContext<RoleServer>,
829 ) -> Result<CallToolResult, ErrorData> {
830 let principal = self.principal(&ctx)?;
831 Ok(issue::list(&self.services, &principal, args).await)
832 }
833
834 #[tool(
835 description = "获取 issue 详情(issue_id 支持 UUID 或看板内编号)。",
836 annotations(read_only_hint = true)
837 )]
838 async fn issue_get(
839 &self,
840 Parameters(args): Parameters<args::IssueRefArgs>,
841 ctx: RequestContext<RoleServer>,
842 ) -> Result<CallToolResult, ErrorData> {
843 let principal = self.principal(&ctx)?;
844 Ok(issue::get(&self.services, &principal, args).await)
845 }
846
847 #[tool(description = "创建 issue。")]
848 async fn issue_create(
849 &self,
850 Parameters(args): Parameters<args::IssueCreateArgs>,
851 ctx: RequestContext<RoleServer>,
852 ) -> Result<CallToolResult, ErrorData> {
853 let principal = self.principal(&ctx)?;
854 Ok(issue::create(&self.services, &principal, args).await)
855 }
856
857 #[tool(description = "更新 issue(作者或看板管理员)。")]
858 async fn issue_update(
859 &self,
860 Parameters(args): Parameters<args::IssueUpdateArgs>,
861 ctx: RequestContext<RoleServer>,
862 ) -> Result<CallToolResult, ErrorData> {
863 let principal = self.principal(&ctx)?;
864 Ok(issue::update(&self.services, &principal, args).await)
865 }
866
867 #[tool(
868 description = "删除 issue(作者或看板管理员)。",
869 annotations(destructive_hint = true)
870 )]
871 async fn issue_delete(
872 &self,
873 Parameters(args): Parameters<args::IssueRefArgs>,
874 ctx: RequestContext<RoleServer>,
875 ) -> Result<CallToolResult, ErrorData> {
876 let principal = self.principal(&ctx)?;
877 Ok(issue::delete(&self.services, &principal, args).await)
878 }
879
880 #[tool(description = "关闭 issue。")]
881 async fn issue_close(
882 &self,
883 Parameters(args): Parameters<args::IssueRefArgs>,
884 ctx: RequestContext<RoleServer>,
885 ) -> Result<CallToolResult, ErrorData> {
886 let principal = self.principal(&ctx)?;
887 Ok(issue::set_closed(&self.services, &principal, args, true).await)
888 }
889
890 #[tool(description = "重新打开 issue。")]
891 async fn issue_reopen(
892 &self,
893 Parameters(args): Parameters<args::IssueRefArgs>,
894 ctx: RequestContext<RoleServer>,
895 ) -> Result<CallToolResult, ErrorData> {
896 let principal = self.principal(&ctx)?;
897 Ok(issue::set_closed(&self.services, &principal, args, false).await)
898 }
899
900 #[tool(description = "列出 issue 评论。", annotations(read_only_hint = true))]
901 async fn issue_comment_list(
902 &self,
903 Parameters(args): Parameters<args::IssueCommentListArgs>,
904 ctx: RequestContext<RoleServer>,
905 ) -> Result<CallToolResult, ErrorData> {
906 let principal = self.principal(&ctx)?;
907 Ok(issue::comment_list(&self.services, &principal, args).await)
908 }
909
910 #[tool(description = "发表 issue 评论。")]
911 async fn issue_comment_create(
912 &self,
913 Parameters(args): Parameters<args::IssueCommentCreateArgs>,
914 ctx: RequestContext<RoleServer>,
915 ) -> Result<CallToolResult, ErrorData> {
916 let principal = self.principal(&ctx)?;
917 Ok(issue::comment_create(&self.services, &principal, args).await)
918 }
919
920 #[tool(description = "更新 issue 评论(作者或看板管理员)。")]
921 async fn issue_comment_update(
922 &self,
923 Parameters(args): Parameters<args::IssueCommentUpdateArgs>,
924 ctx: RequestContext<RoleServer>,
925 ) -> Result<CallToolResult, ErrorData> {
926 let principal = self.principal(&ctx)?;
927 Ok(issue::comment_update(&self.services, &principal, args).await)
928 }
929
930 #[tool(
931 description = "删除 issue 评论(作者或看板管理员)。",
932 annotations(destructive_hint = true)
933 )]
934 async fn issue_comment_delete(
935 &self,
936 Parameters(args): Parameters<args::IssueCommentRefArgs>,
937 ctx: RequestContext<RoleServer>,
938 ) -> Result<CallToolResult, ErrorData> {
939 let principal = self.principal(&ctx)?;
940 Ok(issue::comment_delete(&self.services, &principal, args).await)
941 }
942
943 #[tool(description = "列出看板标签。", annotations(read_only_hint = true))]
944 async fn issue_label_list(
945 &self,
946 Parameters(args): Parameters<args::IssueLabelListArgs>,
947 ctx: RequestContext<RoleServer>,
948 ) -> Result<CallToolResult, ErrorData> {
949 let principal = self.principal(&ctx)?;
950 Ok(issue::label_list(&self.services, &principal, args).await)
951 }
952
953 #[tool(description = "创建看板标签(需要看板管理权限)。")]
954 async fn issue_label_create(
955 &self,
956 Parameters(args): Parameters<args::IssueLabelCreateArgs>,
957 ctx: RequestContext<RoleServer>,
958 ) -> Result<CallToolResult, ErrorData> {
959 let principal = self.principal(&ctx)?;
960 Ok(issue::label_create(&self.services, &principal, args).await)
961 }
962
963 #[tool(description = "更新看板标签(需要看板管理权限)。")]
964 async fn issue_label_update(
965 &self,
966 Parameters(args): Parameters<args::IssueLabelUpdateArgs>,
967 ctx: RequestContext<RoleServer>,
968 ) -> Result<CallToolResult, ErrorData> {
969 let principal = self.principal(&ctx)?;
970 Ok(issue::label_update(&self.services, &principal, args).await)
971 }
972
973 #[tool(
974 description = "删除看板标签(需要看板管理权限)。",
975 annotations(destructive_hint = true)
976 )]
977 async fn issue_label_delete(
978 &self,
979 Parameters(args): Parameters<args::IssueLabelRefArgs>,
980 ctx: RequestContext<RoleServer>,
981 ) -> Result<CallToolResult, ErrorData> {
982 let principal = self.principal(&ctx)?;
983 Ok(issue::label_delete(&self.services, &principal, args).await)
984 }
985
986 #[tool(description = "给 issue 绑定标签。")]
987 async fn issue_label_bind(
988 &self,
989 Parameters(args): Parameters<args::IssueLabelBindArgs>,
990 ctx: RequestContext<RoleServer>,
991 ) -> Result<CallToolResult, ErrorData> {
992 let principal = self.principal(&ctx)?;
993 Ok(issue::label_bind(&self.services, &principal, args).await)
994 }
995
996 #[tool(description = "解绑 issue 标签。")]
997 async fn issue_label_unbind(
998 &self,
999 Parameters(args): Parameters<args::IssueLabelBindArgs>,
1000 ctx: RequestContext<RoleServer>,
1001 ) -> Result<CallToolResult, ErrorData> {
1002 let principal = self.principal(&ctx)?;
1003 Ok(issue::label_unbind(&self.services, &principal, args).await)
1004 }
1005
1006 #[tool(description = "列出看板里程碑。", annotations(read_only_hint = true))]
1007 async fn issue_milestone_list(
1008 &self,
1009 Parameters(args): Parameters<args::IssueMilestoneListArgs>,
1010 ctx: RequestContext<RoleServer>,
1011 ) -> Result<CallToolResult, ErrorData> {
1012 let principal = self.principal(&ctx)?;
1013 Ok(issue::milestone_list(&self.services, &principal, args).await)
1014 }
1015
1016 #[tool(description = "创建看板里程碑(需要看板管理权限)。")]
1017 async fn issue_milestone_create(
1018 &self,
1019 Parameters(args): Parameters<args::IssueMilestoneCreateArgs>,
1020 ctx: RequestContext<RoleServer>,
1021 ) -> Result<CallToolResult, ErrorData> {
1022 let principal = self.principal(&ctx)?;
1023 Ok(issue::milestone_create(&self.services, &principal, args).await)
1024 }
1025
1026 #[tool(description = "更新看板里程碑(需要看板管理权限)。")]
1027 async fn issue_milestone_update(
1028 &self,
1029 Parameters(args): Parameters<args::IssueMilestoneUpdateArgs>,
1030 ctx: RequestContext<RoleServer>,
1031 ) -> Result<CallToolResult, ErrorData> {
1032 let principal = self.principal(&ctx)?;
1033 Ok(issue::milestone_update(&self.services, &principal, args).await)
1034 }
1035
1036 #[tool(
1037 description = "删除看板里程碑(需要看板管理权限)。",
1038 annotations(destructive_hint = true)
1039 )]
1040 async fn issue_milestone_delete(
1041 &self,
1042 Parameters(args): Parameters<args::IssueMilestoneRefArgs>,
1043 ctx: RequestContext<RoleServer>,
1044 ) -> Result<CallToolResult, ErrorData> {
1045 let principal = self.principal(&ctx)?;
1046 Ok(issue::milestone_delete(&self.services, &principal, args).await)
1047 }
1048
1049 #[tool(
1050 description = "列出 issue 成员(参与者/关注者等)。",
1051 annotations(read_only_hint = true)
1052 )]
1053 async fn issue_member_list(
1054 &self,
1055 Parameters(args): Parameters<args::IssueMemberListArgs>,
1056 ctx: RequestContext<RoleServer>,
1057 ) -> Result<CallToolResult, ErrorData> {
1058 let principal = self.principal(&ctx)?;
1059 Ok(issue::member_list(&self.services, &principal, args).await)
1060 }
1061
1062 #[tool(description = "添加 issue 成员(需要看板管理权限)。")]
1063 async fn issue_member_create(
1064 &self,
1065 Parameters(args): Parameters<args::IssueMemberCreateArgs>,
1066 ctx: RequestContext<RoleServer>,
1067 ) -> Result<CallToolResult, ErrorData> {
1068 let principal = self.principal(&ctx)?;
1069 Ok(issue::member_create(&self.services, &principal, args).await)
1070 }
1071
1072 #[tool(
1073 description = "移除 issue 成员(需要看板管理权限)。",
1074 annotations(destructive_hint = true)
1075 )]
1076 async fn issue_member_delete(
1077 &self,
1078 Parameters(args): Parameters<args::IssueMemberRefArgs>,
1079 ctx: RequestContext<RoleServer>,
1080 ) -> Result<CallToolResult, ErrorData> {
1081 let principal = self.principal(&ctx)?;
1082 Ok(issue::member_delete(&self.services, &principal, args).await)
1083 }
1084
1085 #[tool(description = "列出看板置顶。", annotations(read_only_hint = true))]
1086 async fn issue_pin_list(
1087 &self,
1088 Parameters(args): Parameters<args::IssuePinListArgs>,
1089 ctx: RequestContext<RoleServer>,
1090 ) -> Result<CallToolResult, ErrorData> {
1091 let principal = self.principal(&ctx)?;
1092 Ok(issue::pin_list(&self.services, &principal, args).await)
1093 }
1094
1095 #[tool(description = "置顶 issue(需要看板管理权限)。")]
1096 async fn issue_pin_create(
1097 &self,
1098 Parameters(args): Parameters<args::IssuePinCreateArgs>,
1099 ctx: RequestContext<RoleServer>,
1100 ) -> Result<CallToolResult, ErrorData> {
1101 let principal = self.principal(&ctx)?;
1102 Ok(issue::pin_create(&self.services, &principal, args).await)
1103 }
1104
1105 #[tool(description = "调整置顶顺序(需要看板管理权限)。")]
1106 async fn issue_pin_update(
1107 &self,
1108 Parameters(args): Parameters<args::IssuePinUpdateArgs>,
1109 ctx: RequestContext<RoleServer>,
1110 ) -> Result<CallToolResult, ErrorData> {
1111 let principal = self.principal(&ctx)?;
1112 Ok(issue::pin_update(&self.services, &principal, args).await)
1113 }
1114
1115 #[tool(
1116 description = "取消置顶(需要看板管理权限)。",
1117 annotations(destructive_hint = true)
1118 )]
1119 async fn issue_pin_delete(
1120 &self,
1121 Parameters(args): Parameters<args::IssuePinRefArgs>,
1122 ctx: RequestContext<RoleServer>,
1123 ) -> Result<CallToolResult, ErrorData> {
1124 let principal = self.principal(&ctx)?;
1125 Ok(issue::pin_delete(&self.services, &principal, args).await)
1126 }
1127
1128 #[tool(description = "列出 issue 依赖。", annotations(read_only_hint = true))]
1129 async fn issue_dependency_list(
1130 &self,
1131 Parameters(args): Parameters<args::IssueDependencyListArgs>,
1132 ctx: RequestContext<RoleServer>,
1133 ) -> Result<CallToolResult, ErrorData> {
1134 let principal = self.principal(&ctx)?;
1135 Ok(issue::dependency_list(&self.services, &principal, args).await)
1136 }
1137
1138 #[tool(description = "添加 issue 依赖(需要看板管理权限)。")]
1139 async fn issue_dependency_create(
1140 &self,
1141 Parameters(args): Parameters<args::IssueDependencyCreateArgs>,
1142 ctx: RequestContext<RoleServer>,
1143 ) -> Result<CallToolResult, ErrorData> {
1144 let principal = self.principal(&ctx)?;
1145 Ok(issue::dependency_create(&self.services, &principal, args).await)
1146 }
1147
1148 #[tool(
1149 description = "删除 issue 依赖(需要看板管理权限)。",
1150 annotations(destructive_hint = true)
1151 )]
1152 async fn issue_dependency_delete(
1153 &self,
1154 Parameters(args): Parameters<args::IssueDependencyRefArgs>,
1155 ctx: RequestContext<RoleServer>,
1156 ) -> Result<CallToolResult, ErrorData> {
1157 let principal = self.principal(&ctx)?;
1158 Ok(issue::dependency_delete(&self.services, &principal, args).await)
1159 }
1160
1161 #[tool(
1162 description = "列出仓库的合并请求。",
1163 annotations(read_only_hint = true)
1164 )]
1165 async fn pull_list(
1166 &self,
1167 Parameters(args): Parameters<args::PullListArgs>,
1168 ctx: RequestContext<RoleServer>,
1169 ) -> Result<CallToolResult, ErrorData> {
1170 let principal = self.principal(&ctx)?;
1171 Ok(pull::list(&self.services, &principal, args).await)
1172 }
1173
1174 #[tool(
1175 description = "获取合并请求详情(pull 支持编号或 UUID)。",
1176 annotations(read_only_hint = true)
1177 )]
1178 async fn pull_get(
1179 &self,
1180 Parameters(args): Parameters<args::PullRefArgs>,
1181 ctx: RequestContext<RoleServer>,
1182 ) -> Result<CallToolResult, ErrorData> {
1183 let principal = self.principal(&ctx)?;
1184 Ok(pull::get(&self.services, &principal, args).await)
1185 }
1186
1187 #[tool(description = "创建合并请求(需要仓库写权限)。")]
1188 async fn pull_create(
1189 &self,
1190 Parameters(args): Parameters<args::PullCreateArgs>,
1191 ctx: RequestContext<RoleServer>,
1192 ) -> Result<CallToolResult, ErrorData> {
1193 let principal = self.principal(&ctx)?;
1194 Ok(pull::create(&self.services, &principal, args).await)
1195 }
1196
1197 #[tool(description = "更新合并请求(需要仓库写权限)。")]
1198 async fn pull_update(
1199 &self,
1200 Parameters(args): Parameters<args::PullUpdateArgs>,
1201 ctx: RequestContext<RoleServer>,
1202 ) -> Result<CallToolResult, ErrorData> {
1203 let principal = self.principal(&ctx)?;
1204 Ok(pull::update(&self.services, &principal, args).await)
1205 }
1206
1207 #[tool(description = "关闭合并请求(作者或仓库管理员)。")]
1208 async fn pull_close(
1209 &self,
1210 Parameters(args): Parameters<args::PullRefArgs>,
1211 ctx: RequestContext<RoleServer>,
1212 ) -> Result<CallToolResult, ErrorData> {
1213 let principal = self.principal(&ctx)?;
1214 Ok(pull::set_state(&self.services, &principal, args, true).await)
1215 }
1216
1217 #[tool(description = "重新打开合并请求(作者或仓库管理员)。")]
1218 async fn pull_reopen(
1219 &self,
1220 Parameters(args): Parameters<args::PullRefArgs>,
1221 ctx: RequestContext<RoleServer>,
1222 ) -> Result<CallToolResult, ErrorData> {
1223 let principal = self.principal(&ctx)?;
1224 Ok(pull::set_state(&self.services, &principal, args, false).await)
1225 }
1226
1227 #[tool(
1228 description = "删除合并请求(仅仓库管理员)。",
1229 annotations(destructive_hint = true)
1230 )]
1231 async fn pull_delete(
1232 &self,
1233 Parameters(args): Parameters<args::PullRefArgs>,
1234 ctx: RequestContext<RoleServer>,
1235 ) -> Result<CallToolResult, ErrorData> {
1236 let principal = self.principal(&ctx)?;
1237 Ok(pull::delete(&self.services, &principal, args).await)
1238 }
1239
1240 #[tool(
1241 description = "合并合并请求(受分支保护与审批门槛限制)。",
1242 annotations(destructive_hint = true)
1243 )]
1244 async fn pull_merge(
1245 &self,
1246 Parameters(args): Parameters<args::PullMergeArgs>,
1247 ctx: RequestContext<RoleServer>,
1248 ) -> Result<CallToolResult, ErrorData> {
1249 let principal = self.principal(&ctx)?;
1250 Ok(pull::merge(&self.services, &principal, args).await)
1251 }
1252
1253 #[tool(description = "列出合并请求评论。", annotations(read_only_hint = true))]
1254 async fn pull_comment_list(
1255 &self,
1256 Parameters(args): Parameters<args::PullCommentListArgs>,
1257 ctx: RequestContext<RoleServer>,
1258 ) -> Result<CallToolResult, ErrorData> {
1259 let principal = self.principal(&ctx)?;
1260 Ok(pull::comment_list(&self.services, &principal, args).await)
1261 }
1262
1263 #[tool(description = "发表合并请求评论。")]
1264 async fn pull_comment_create(
1265 &self,
1266 Parameters(args): Parameters<args::PullCommentCreateArgs>,
1267 ctx: RequestContext<RoleServer>,
1268 ) -> Result<CallToolResult, ErrorData> {
1269 let principal = self.principal(&ctx)?;
1270 Ok(pull::comment_create(&self.services, &principal, args).await)
1271 }
1272
1273 #[tool(description = "更新合并请求评论(作者或管理员)。")]
1274 async fn pull_comment_update(
1275 &self,
1276 Parameters(args): Parameters<args::PullCommentUpdateArgs>,
1277 ctx: RequestContext<RoleServer>,
1278 ) -> Result<CallToolResult, ErrorData> {
1279 let principal = self.principal(&ctx)?;
1280 Ok(pull::comment_update(&self.services, &principal, args).await)
1281 }
1282
1283 #[tool(
1284 description = "删除合并请求评论(作者或管理员)。",
1285 annotations(destructive_hint = true)
1286 )]
1287 async fn pull_comment_delete(
1288 &self,
1289 Parameters(args): Parameters<args::PullCommentRefArgs>,
1290 ctx: RequestContext<RoleServer>,
1291 ) -> Result<CallToolResult, ErrorData> {
1292 let principal = self.principal(&ctx)?;
1293 Ok(pull::comment_delete(&self.services, &principal, args).await)
1294 }
1295
1296 #[tool(description = "标记/取消标记评论已解决。")]
1297 async fn pull_comment_resolve(
1298 &self,
1299 Parameters(args): Parameters<args::PullCommentResolveArgs>,
1300 ctx: RequestContext<RoleServer>,
1301 ) -> Result<CallToolResult, ErrorData> {
1302 let principal = self.principal(&ctx)?;
1303 Ok(pull::comment_resolve(&self.services, &principal, args).await)
1304 }
1305
1306 #[tool(
1307 description = "列出合并请求的评审。",
1308 annotations(read_only_hint = true)
1309 )]
1310 async fn pull_review_list(
1311 &self,
1312 Parameters(args): Parameters<args::PullReviewsArgs>,
1313 ctx: RequestContext<RoleServer>,
1314 ) -> Result<CallToolResult, ErrorData> {
1315 let principal = self.principal(&ctx)?;
1316 Ok(pull::review_list(&self.services, &principal, args).await)
1317 }
1318
1319 #[tool(description = "提交评审(approve/request_changes/comment)。")]
1320 async fn pull_review_submit(
1321 &self,
1322 Parameters(args): Parameters<args::PullReviewSubmitArgs>,
1323 ctx: RequestContext<RoleServer>,
1324 ) -> Result<CallToolResult, ErrorData> {
1325 let principal = self.principal(&ctx)?;
1326 Ok(pull::review_submit(&self.services, &principal, args).await)
1327 }
1328
1329 #[tool(description = "驳回评审(需要管理员)。")]
1330 async fn pull_review_dismiss(
1331 &self,
1332 Parameters(args): Parameters<args::PullReviewDismissArgs>,
1333 ctx: RequestContext<RoleServer>,
1334 ) -> Result<CallToolResult, ErrorData> {
1335 let principal = self.principal(&ctx)?;
1336 Ok(pull::review_dismiss(&self.services, &principal, args).await)
1337 }
1338
1339 #[tool(description = "列出评审请求。", annotations(read_only_hint = true))]
1340 async fn pull_reviewer_list(
1341 &self,
1342 Parameters(args): Parameters<args::PullReviewsArgs>,
1343 ctx: RequestContext<RoleServer>,
1344 ) -> Result<CallToolResult, ErrorData> {
1345 let principal = self.principal(&ctx)?;
1346 Ok(pull::reviewer_list(&self.services, &principal, args).await)
1347 }
1348
1349 #[tool(description = "请求评审人(需要仓库写权限)。")]
1350 async fn pull_reviewer_request(
1351 &self,
1352 Parameters(args): Parameters<args::PullReviewerRequestArgs>,
1353 ctx: RequestContext<RoleServer>,
1354 ) -> Result<CallToolResult, ErrorData> {
1355 let principal = self.principal(&ctx)?;
1356 Ok(pull::reviewer_request(&self.services, &principal, args).await)
1357 }
1358
1359 #[tool(description = "取消评审请求(需要仓库写权限)。")]
1360 async fn pull_reviewer_cancel(
1361 &self,
1362 Parameters(args): Parameters<args::PullReviewerCancelArgs>,
1363 ctx: RequestContext<RoleServer>,
1364 ) -> Result<CallToolResult, ErrorData> {
1365 let principal = self.principal(&ctx)?;
1366 Ok(pull::reviewer_cancel(&self.services, &principal, args).await)
1367 }
1368
1369 #[tool(
1370 description = "列出合并请求指派人。",
1371 annotations(read_only_hint = true)
1372 )]
1373 async fn pull_assignee_list(
1374 &self,
1375 Parameters(args): Parameters<args::PullReviewsArgs>,
1376 ctx: RequestContext<RoleServer>,
1377 ) -> Result<CallToolResult, ErrorData> {
1378 let principal = self.principal(&ctx)?;
1379 Ok(pull::assignee_list(&self.services, &principal, args).await)
1380 }
1381
1382 #[tool(description = "指派合并请求负责人(需要仓库写权限)。")]
1383 async fn pull_assignee_add(
1384 &self,
1385 Parameters(args): Parameters<args::PullAssigneeArgs>,
1386 ctx: RequestContext<RoleServer>,
1387 ) -> Result<CallToolResult, ErrorData> {
1388 let principal = self.principal(&ctx)?;
1389 Ok(pull::assignee_add(&self.services, &principal, args).await)
1390 }
1391
1392 #[tool(description = "移除合并请求负责人(需要仓库写权限)。")]
1393 async fn pull_assignee_remove(
1394 &self,
1395 Parameters(args): Parameters<args::PullAssigneeArgs>,
1396 ctx: RequestContext<RoleServer>,
1397 ) -> Result<CallToolResult, ErrorData> {
1398 let principal = self.principal(&ctx)?;
1399 Ok(pull::assignee_remove(&self.services, &principal, args).await)
1400 }
1401
1402 #[tool(description = "列出合并请求标签。", annotations(read_only_hint = true))]
1403 async fn pull_label_list(
1404 &self,
1405 Parameters(args): Parameters<args::PullReviewsArgs>,
1406 ctx: RequestContext<RoleServer>,
1407 ) -> Result<CallToolResult, ErrorData> {
1408 let principal = self.principal(&ctx)?;
1409 Ok(pull::label_list(&self.services, &principal, args).await)
1410 }
1411
1412 #[tool(description = "给合并请求添加标签(需要仓库写权限)。")]
1413 async fn pull_label_attach(
1414 &self,
1415 Parameters(args): Parameters<args::PullLabelArgs>,
1416 ctx: RequestContext<RoleServer>,
1417 ) -> Result<CallToolResult, ErrorData> {
1418 let principal = self.principal(&ctx)?;
1419 Ok(pull::label_attach(&self.services, &principal, args).await)
1420 }
1421
1422 #[tool(description = "移除合并请求标签(需要仓库写权限)。")]
1423 async fn pull_label_detach(
1424 &self,
1425 Parameters(args): Parameters<args::PullLabelArgs>,
1426 ctx: RequestContext<RoleServer>,
1427 ) -> Result<CallToolResult, ErrorData> {
1428 let principal = self.principal(&ctx)?;
1429 Ok(pull::label_detach(&self.services, &principal, args).await)
1430 }
1431
1432 #[tool(
1433 description = "查看合并请求的 CI 检查状态。",
1434 annotations(read_only_hint = true)
1435 )]
1436 async fn pull_checks(
1437 &self,
1438 Parameters(args): Parameters<args::PullReviewsArgs>,
1439 ctx: RequestContext<RoleServer>,
1440 ) -> Result<CallToolResult, ErrorData> {
1441 let principal = self.principal(&ctx)?;
1442 Ok(pull::checks(&self.services, &principal, args).await)
1443 }
1444
1445 #[tool(
1446 description = "查看合并请求的合并门槛(分支保护/审批)。",
1447 annotations(read_only_hint = true)
1448 )]
1449 async fn pull_gate(
1450 &self,
1451 Parameters(args): Parameters<args::PullReviewsArgs>,
1452 ctx: RequestContext<RoleServer>,
1453 ) -> Result<CallToolResult, ErrorData> {
1454 let principal = self.principal(&ctx)?;
1455 Ok(pull::gate(&self.services, &principal, args).await)
1456 }
1457
1458 #[tool(
1459 description = "查看我的合并请求收件箱(created/review-requested/reviewed,scope 用 kebab-case)。",
1460 annotations(read_only_hint = true)
1461 )]
1462 async fn pull_inbox(
1463 &self,
1464 Parameters(args): Parameters<args::PullInboxArgs>,
1465 ctx: RequestContext<RoleServer>,
1466 ) -> Result<CallToolResult, ErrorData> {
1467 let principal = self.principal(&ctx)?;
1468 Ok(pull::inbox(&self.services, &principal, args).await)
1469 }
1470}
Added lib/mcp-api/src/tools/pull.rs View lib/mcp-api/src/tools/pull.rs
@@ -0,0 +1,958 @@
1use git::context::facade::GitContext;
2use pulls::services::comments::CreatePullCommentCmd;
3use pulls::services::inbox::{InboxQuery, InboxScope};
4use pulls::services::labels::AttachLabelCmd;
5use pulls::services::merge::MergePullCmd;
6use pulls::services::pulls::{CreatePullCmd, ListPullQuery, UpdatePullCmd};
7use pulls::services::reviewers::RequestReviewersCmd;
8use pulls::services::reviews::{InlineDraft, ReviewIntent, SubmitReviewCmd};
9use pulls::share::lookup::PullHandle;
10use rmcp::model::CallToolResult;
11use serde_json::json;
12
13use crate::auth::Principal;
14use crate::error::{tool_done, tool_error, tool_message, tool_ok, write_denied};
15use crate::services::McpServices;
16use crate::tools::args;
17use crate::tools::helpers::{page, parse_enum, repo_read, repo_write};
18
19async fn handle_for(
20 services: &McpServices,
21 principal: &Principal,
22 space: &str,
23 repo: &str,
24 pull_ref: &str,
25 write: bool,
26) -> Result<PullHandle, CallToolResult> {
27 let ctx: GitContext = if write {
28 repo_write(services, principal.uid, space, repo).await?
29 } else {
30 repo_read(services, principal.uid, space, repo).await?
31 };
32 let pull = pulls::share::lookup::find_pull(&services.pulls, ctx.model.id, pull_ref.trim())
33 .await
34 .map_err(tool_error)?;
35 let admin = pulls::share::lookup::is_admin(&services.pulls, principal.uid, &ctx).await;
36 Ok(PullHandle {
37 ctx,
38 pull,
39 caller: Some(principal.uid),
40 admin,
41 })
42}
43
44async fn detail(
45 services: &McpServices,
46 space: &str,
47 repo: &str,
48 handle: &PullHandle,
49) -> CallToolResult {
50 match services
51 .pulls
52 .pull_detail(
53 space.trim().to_string(),
54 repo.trim().to_string(),
55 &handle.ctx,
56 &handle.pull,
57 )
58 .await
59 {
60 Ok(dto) => tool_ok(&dto),
61 Err(error) => tool_error(error),
62 }
63}
64
65pub(crate) async fn list(
66 services: &McpServices,
67 principal: &Principal,
68 args: args::PullListArgs,
69) -> CallToolResult {
70 let ctx = match repo_read(services, principal.uid, &args.space, &args.repo).await {
71 Ok(ctx) => ctx,
72 Err(error) => return error,
73 };
74 let query = ListPullQuery {
75 state: args.state,
76 author: args.author,
77 base: args.base,
78 head: args.head,
79 draft: args.draft,
80 search: args.search,
81 sort: args.sort,
82 order: args.order,
83 limit: args.limit,
84 offset: args.offset,
85 };
86 match services.pulls.list_pulls(ctx.model.id, query).await {
87 Ok(result) => tool_ok(&result),
88 Err(error) => tool_error(error),
89 }
90}
91
92pub(crate) async fn create(
93 services: &McpServices,
94 principal: &Principal,
95 args: args::PullCreateArgs,
96) -> CallToolResult {
97 if !principal.can_write() {
98 return write_denied();
99 }
100 let ctx = match repo_write(services, principal.uid, &args.space, &args.repo).await {
101 Ok(ctx) => ctx,
102 Err(error) => return error,
103 };
104 let cmd = CreatePullCmd {
105 title: args.title,
106 body: args.body,
107 head_branch: args.head_branch,
108 base_branch: args.base_branch,
109 draft: args.draft,
110 issue_id: args.issue_id,
111 };
112 let space = args.space.trim().to_string();
113 let repo = args.repo.trim().to_string();
114 match services
115 .pulls
116 .create_pull(space.clone(), repo.clone(), &ctx, principal.uid, cmd)
117 .await
118 {
119 Ok(model) => {
120 let handle = PullHandle {
121 ctx,
122 pull: model,
123 caller: Some(principal.uid),
124 admin: false,
125 };
126 detail(services, &space, &repo, &handle).await
127 }
128 Err(error) => tool_error(error),
129 }
130}
131
132pub(crate) async fn get(
133 services: &McpServices,
134 principal: &Principal,
135 args: args::PullRefArgs,
136) -> CallToolResult {
137 let handle = match handle_for(
138 services,
139 principal,
140 &args.space,
141 &args.repo,
142 &args.pull,
143 false,
144 )
145 .await
146 {
147 Ok(handle) => handle,
148 Err(error) => return error,
149 };
150 detail(services, &args.space, &args.repo, &handle).await
151}
152
153pub(crate) async fn update(
154 services: &McpServices,
155 principal: &Principal,
156 args: args::PullUpdateArgs,
157) -> CallToolResult {
158 if !principal.can_write() {
159 return write_denied();
160 }
161 let handle = match handle_for(
162 services,
163 principal,
164 &args.space,
165 &args.repo,
166 &args.pull,
167 true,
168 )
169 .await
170 {
171 Ok(handle) => handle,
172 Err(error) => return error,
173 };
174 let cmd = UpdatePullCmd {
175 title: args.title,
176 body: args.body.map(Some),
177 base_branch: args.base_branch,
178 draft: args.draft,
179 auto_merge: args.auto_merge.map(Some),
180 issue_id: None,
181 };
182 let space = args.space.trim().to_string();
183 let repo = args.repo.trim().to_string();
184 match services
185 .pulls
186 .update_pull(space.clone(), repo.clone(), &handle, cmd)
187 .await
188 {
189 Ok(model) => {
190 let handle = PullHandle {
191 ctx: handle.ctx,
192 pull: model,
193 caller: Some(principal.uid),
194 admin: handle.admin,
195 };
196 detail(services, &space, &repo, &handle).await
197 }
198 Err(error) => tool_error(error),
199 }
200}
201
202pub(crate) async fn set_state(
203 services: &McpServices,
204 principal: &Principal,
205 args: args::PullRefArgs,
206 closed: bool,
207) -> CallToolResult {
208 if !principal.can_write() {
209 return write_denied();
210 }
211 let handle = match handle_for(
212 services,
213 principal,
214 &args.space,
215 &args.repo,
216 &args.pull,
217 true,
218 )
219 .await
220 {
221 Ok(handle) => handle,
222 Err(error) => return error,
223 };
224 match services
225 .pulls
226 .set_state(&handle.pull, principal.uid, closed)
227 .await
228 {
229 Ok(model) => {
230 let handle = PullHandle {
231 ctx: handle.ctx,
232 pull: model,
233 caller: Some(principal.uid),
234 admin: handle.admin,
235 };
236 detail(services, &args.space, &args.repo, &handle).await
237 }
238 Err(error) => tool_error(error),
239 }
240}
241
242pub(crate) async fn delete(
243 services: &McpServices,
244 principal: &Principal,
245 args: args::PullRefArgs,
246) -> CallToolResult {
247 if !principal.can_write() {
248 return write_denied();
249 }
250 let handle = match handle_for(
251 services,
252 principal,
253 &args.space,
254 &args.repo,
255 &args.pull,
256 true,
257 )
258 .await
259 {
260 Ok(handle) => handle,
261 Err(error) => return error,
262 };
263 if !handle.admin {
264 return tool_message("仅仓库管理员可以删除合并请求");
265 }
266 match services
267 .pulls
268 .delete_pull(&handle.pull, principal.uid)
269 .await
270 {
271 Ok(()) => tool_done(),
272 Err(error) => tool_error(error),
273 }
274}
275
276pub(crate) async fn merge(
277 services: &McpServices,
278 principal: &Principal,
279 args: args::PullMergeArgs,
280) -> CallToolResult {
281 if !principal.can_write() {
282 return write_denied();
283 }
284 let handle = match handle_for(
285 services,
286 principal,
287 &args.space,
288 &args.repo,
289 &args.pull,
290 true,
291 )
292 .await
293 {
294 Ok(handle) => handle,
295 Err(error) => return error,
296 };
297 let method = match args.method.as_deref() {
298 Some(raw) => match parse_enum::<models::pull_request::pull_requests::MergeMethod>(raw) {
299 Ok(method) => Some(method),
300 Err(error) => return error,
301 },
302 None => None,
303 };
304 let cmd = MergePullCmd {
305 method,
306 title: args.title,
307 message: args.message,
308 fast_forward_only: args.fast_forward_only,
309 expect_head_sha: args.expect_head_sha,
310 delete_branch_after_merge: args.delete_branch_after_merge,
311 };
312 let space = args.space.trim().to_string();
313 let repo = args.repo.trim().to_string();
314 match services
315 .pulls
316 .merge_pull(space.clone(), repo.clone(), &handle, cmd)
317 .await
318 {
319 Ok((model, result, summary)) => {
320 let handle = PullHandle {
321 ctx: handle.ctx,
322 pull: model,
323 caller: Some(principal.uid),
324 admin: handle.admin,
325 };
326 match services
327 .pulls
328 .pull_detail(space, repo, &handle.ctx, &handle.pull)
329 .await
330 {
331 Ok(dto) => tool_ok(&json!({ "pull": dto, "result": result, "summary": summary })),
332 Err(error) => tool_error(error),
333 }
334 }
335 Err(error) => tool_error(error),
336 }
337}
338
339pub(crate) async fn comment_list(
340 services: &McpServices,
341 principal: &Principal,
342 args: args::PullCommentListArgs,
343) -> CallToolResult {
344 let handle = match handle_for(
345 services,
346 principal,
347 &args.space,
348 &args.repo,
349 &args.pull,
350 false,
351 )
352 .await
353 {
354 Ok(handle) => handle,
355 Err(error) => return error,
356 };
357 match services
358 .pulls
359 .list_comments(handle.pull.id, page(args.limit, args.offset, None))
360 .await
361 {
362 Ok(result) => tool_ok(&result),
363 Err(error) => tool_error(error),
364 }
365}
366
367pub(crate) async fn comment_create(
368 services: &McpServices,
369 principal: &Principal,
370 args: args::PullCommentCreateArgs,
371) -> CallToolResult {
372 if !principal.can_write() {
373 return write_denied();
374 }
375 let handle = match handle_for(
376 services,
377 principal,
378 &args.space,
379 &args.repo,
380 &args.pull,
381 true,
382 )
383 .await
384 {
385 Ok(handle) => handle,
386 Err(error) => return error,
387 };
388 let cmd = CreatePullCommentCmd {
389 body: args.body,
390 path: args.path,
391 line: args.line,
392 side: args.side,
393 commit_sha: args.commit_sha,
394 review_id: None,
395 parent_comment_id: args.parent_comment_id,
396 };
397 match services
398 .pulls
399 .create_comment(&handle.ctx, &handle.pull, principal.uid, cmd)
400 .await
401 {
402 Ok(model) => tool_ok(&json!({ "id": model.id })),
403 Err(error) => tool_error(error),
404 }
405}
406
407pub(crate) async fn comment_update(
408 services: &McpServices,
409 principal: &Principal,
410 args: args::PullCommentUpdateArgs,
411) -> CallToolResult {
412 if !principal.can_write() {
413 return write_denied();
414 }
415 let handle = match handle_for(
416 services,
417 principal,
418 &args.space,
419 &args.repo,
420 &args.pull,
421 true,
422 )
423 .await
424 {
425 Ok(handle) => handle,
426 Err(error) => return error,
427 };
428 match services
429 .pulls
430 .update_comment(
431 &handle.pull,
432 args.comment_id,
433 principal.uid,
434 handle.admin,
435 args.body,
436 )
437 .await
438 {
439 Ok(model) => tool_ok(&json!({ "id": model.id })),
440 Err(error) => tool_error(error),
441 }
442}
443
444pub(crate) async fn comment_delete(
445 services: &McpServices,
446 principal: &Principal,
447 args: args::PullCommentRefArgs,
448) -> CallToolResult {
449 if !principal.can_write() {
450 return write_denied();
451 }
452 let handle = match handle_for(
453 services,
454 principal,
455 &args.space,
456 &args.repo,
457 &args.pull,
458 true,
459 )
460 .await
461 {
462 Ok(handle) => handle,
463 Err(error) => return error,
464 };
465 match services
466 .pulls
467 .delete_comment(&handle.pull, args.comment_id, principal.uid, handle.admin)
468 .await
469 {
470 Ok(()) => tool_done(),
471 Err(error) => tool_error(error),
472 }
473}
474
475pub(crate) async fn comment_resolve(
476 services: &McpServices,
477 principal: &Principal,
478 args: args::PullCommentResolveArgs,
479) -> CallToolResult {
480 if !principal.can_write() {
481 return write_denied();
482 }
483 let handle = match handle_for(
484 services,
485 principal,
486 &args.space,
487 &args.repo,
488 &args.pull,
489 true,
490 )
491 .await
492 {
493 Ok(handle) => handle,
494 Err(error) => return error,
495 };
496 match services
497 .pulls
498 .set_comment_resolved(&handle.pull, args.comment_id, principal.uid, args.resolved)
499 .await
500 {
501 Ok(model) => tool_ok(&json!({ "id": model.id, "resolved": args.resolved })),
502 Err(error) => tool_error(error),
503 }
504}
505
506pub(crate) async fn review_list(
507 services: &McpServices,
508 principal: &Principal,
509 args: args::PullReviewsArgs,
510) -> CallToolResult {
511 let handle = match handle_for(
512 services,
513 principal,
514 &args.space,
515 &args.repo,
516 &args.pull,
517 false,
518 )
519 .await
520 {
521 Ok(handle) => handle,
522 Err(error) => return error,
523 };
524 match services
525 .pulls
526 .list_reviews(handle.pull.id, Some(principal.uid))
527 .await
528 {
529 Ok(result) => tool_ok(&result),
530 Err(error) => tool_error(error),
531 }
532}
533
534pub(crate) async fn review_submit(
535 services: &McpServices,
536 principal: &Principal,
537 args: args::PullReviewSubmitArgs,
538) -> CallToolResult {
539 if !principal.can_write() {
540 return write_denied();
541 }
542 let state = match parse_enum::<ReviewIntent>(&args.state) {
543 Ok(state) => state,
544 Err(error) => return error,
545 };
546 let handle = match handle_for(
547 services,
548 principal,
549 &args.space,
550 &args.repo,
551 &args.pull,
552 true,
553 )
554 .await
555 {
556 Ok(handle) => handle,
557 Err(error) => return error,
558 };
559 let cmd = SubmitReviewCmd {
560 state,
561 body: args.body,
562 comments: Vec::<InlineDraft>::new(),
563 };
564 match services
565 .pulls
566 .submit_review(&handle.ctx, &handle.pull, principal.uid, handle.admin, cmd)
567 .await
568 {
569 Ok(model) => tool_ok(&json!({ "id": model.id })),
570 Err(error) => tool_error(error),
571 }
572}
573
574pub(crate) async fn review_dismiss(
575 services: &McpServices,
576 principal: &Principal,
577 args: args::PullReviewDismissArgs,
578) -> CallToolResult {
579 if !principal.can_write() {
580 return write_denied();
581 }
582 let handle = match handle_for(
583 services,
584 principal,
585 &args.space,
586 &args.repo,
587 &args.pull,
588 true,
589 )
590 .await
591 {
592 Ok(handle) => handle,
593 Err(error) => return error,
594 };
595 match services
596 .pulls
597 .dismiss_review(&handle.pull, args.review_id, principal.uid)
598 .await
599 {
600 Ok(model) => tool_ok(&json!({ "id": model.id })),
601 Err(error) => tool_error(error),
602 }
603}
604
605pub(crate) async fn reviewer_list(
606 services: &McpServices,
607 principal: &Principal,
608 args: args::PullReviewsArgs,
609) -> CallToolResult {
610 let handle = match handle_for(
611 services,
612 principal,
613 &args.space,
614 &args.repo,
615 &args.pull,
616 false,
617 )
618 .await
619 {
620 Ok(handle) => handle,
621 Err(error) => return error,
622 };
623 match services.pulls.list_reviewers(handle.pull.id).await {
624 Ok(result) => tool_ok(&result),
625 Err(error) => tool_error(error),
626 }
627}
628
629pub(crate) async fn reviewer_request(
630 services: &McpServices,
631 principal: &Principal,
632 args: args::PullReviewerRequestArgs,
633) -> CallToolResult {
634 if !principal.can_write() {
635 return write_denied();
636 }
637 let handle = match handle_for(
638 services,
639 principal,
640 &args.space,
641 &args.repo,
642 &args.pull,
643 true,
644 )
645 .await
646 {
647 Ok(handle) => handle,
648 Err(error) => return error,
649 };
650 let cmd = RequestReviewersCmd {
651 reviewers: args.reviewers,
652 required: args.required.unwrap_or(true),
653 };
654 match services
655 .pulls
656 .request_reviewers(&handle.pull, principal.uid, cmd)
657 .await
658 {
659 Ok(result) => tool_ok(&result),
660 Err(error) => tool_error(error),
661 }
662}
663
664pub(crate) async fn reviewer_cancel(
665 services: &McpServices,
666 principal: &Principal,
667 args: args::PullReviewerCancelArgs,
668) -> CallToolResult {
669 if !principal.can_write() {
670 return write_denied();
671 }
672 let handle = match handle_for(
673 services,
674 principal,
675 &args.space,
676 &args.repo,
677 &args.pull,
678 true,
679 )
680 .await
681 {
682 Ok(handle) => handle,
683 Err(error) => return error,
684 };
685 match services
686 .pulls
687 .cancel_reviewer_request(&handle.pull, principal.uid, args.reviewer_id)
688 .await
689 {
690 Ok(result) => tool_ok(&result),
691 Err(error) => tool_error(error),
692 }
693}
694
695pub(crate) async fn assignee_list(
696 services: &McpServices,
697 principal: &Principal,
698 args: args::PullReviewsArgs,
699) -> CallToolResult {
700 let handle = match handle_for(
701 services,
702 principal,
703 &args.space,
704 &args.repo,
705 &args.pull,
706 false,
707 )
708 .await
709 {
710 Ok(handle) => handle,
711 Err(error) => return error,
712 };
713 match services.pulls.list_assignees(handle.pull.id).await {
714 Ok(result) => tool_ok(&result),
715 Err(error) => tool_error(error),
716 }
717}
718
719pub(crate) async fn assignee_add(
720 services: &McpServices,
721 principal: &Principal,
722 args: args::PullAssigneeArgs,
723) -> CallToolResult {
724 if !principal.can_write() {
725 return write_denied();
726 }
727 let handle = match handle_for(
728 services,
729 principal,
730 &args.space,
731 &args.repo,
732 &args.pull,
733 true,
734 )
735 .await
736 {
737 Ok(handle) => handle,
738 Err(error) => return error,
739 };
740 match services
741 .pulls
742 .add_assignee(&handle.pull, principal.uid, &args.user)
743 .await
744 {
745 Ok(result) => tool_ok(&result),
746 Err(error) => tool_error(error),
747 }
748}
749
750pub(crate) async fn assignee_remove(
751 services: &McpServices,
752 principal: &Principal,
753 args: args::PullAssigneeArgs,
754) -> CallToolResult {
755 if !principal.can_write() {
756 return write_denied();
757 }
758 let handle = match handle_for(
759 services,
760 principal,
761 &args.space,
762 &args.repo,
763 &args.pull,
764 true,
765 )
766 .await
767 {
768 Ok(handle) => handle,
769 Err(error) => return error,
770 };
771 match services
772 .pulls
773 .remove_assignee(&handle.pull, principal.uid, &args.user)
774 .await
775 {
776 Ok(result) => tool_ok(&result),
777 Err(error) => tool_error(error),
778 }
779}
780
781pub(crate) async fn label_list(
782 services: &McpServices,
783 principal: &Principal,
784 args: args::PullReviewsArgs,
785) -> CallToolResult {
786 let handle = match handle_for(
787 services,
788 principal,
789 &args.space,
790 &args.repo,
791 &args.pull,
792 false,
793 )
794 .await
795 {
796 Ok(handle) => handle,
797 Err(error) => return error,
798 };
799 match services.pulls.list_labels(handle.pull.id).await {
800 Ok(result) => tool_ok(&result),
801 Err(error) => tool_error(error),
802 }
803}
804
805pub(crate) async fn label_attach(
806 services: &McpServices,
807 principal: &Principal,
808 args: args::PullLabelArgs,
809) -> CallToolResult {
810 if !principal.can_write() {
811 return write_denied();
812 }
813 let handle = match handle_for(
814 services,
815 principal,
816 &args.space,
817 &args.repo,
818 &args.pull,
819 true,
820 )
821 .await
822 {
823 Ok(handle) => handle,
824 Err(error) => return error,
825 };
826 let cmd = AttachLabelCmd { label: args.label };
827 match services
828 .pulls
829 .attach_label(&handle.pull, handle.ctx.model.dir_id, principal.uid, cmd)
830 .await
831 {
832 Ok(result) => tool_ok(&result),
833 Err(error) => tool_error(error),
834 }
835}
836
837pub(crate) async fn label_detach(
838 services: &McpServices,
839 principal: &Principal,
840 args: args::PullLabelArgs,
841) -> CallToolResult {
842 if !principal.can_write() {
843 return write_denied();
844 }
845 let handle = match handle_for(
846 services,
847 principal,
848 &args.space,
849 &args.repo,
850 &args.pull,
851 true,
852 )
853 .await
854 {
855 Ok(handle) => handle,
856 Err(error) => return error,
857 };
858 match services
859 .pulls
860 .detach_label(
861 &handle.pull,
862 handle.ctx.model.dir_id,
863 principal.uid,
864 &args.label,
865 )
866 .await
867 {
868 Ok(result) => tool_ok(&result),
869 Err(error) => tool_error(error),
870 }
871}
872
873pub(crate) async fn checks(
874 services: &McpServices,
875 principal: &Principal,
876 args: args::PullReviewsArgs,
877) -> CallToolResult {
878 let handle = match handle_for(
879 services,
880 principal,
881 &args.space,
882 &args.repo,
883 &args.pull,
884 false,
885 )
886 .await
887 {
888 Ok(handle) => handle,
889 Err(error) => return error,
890 };
891 match services
892 .pulls
893 .pull_checks(
894 args.space.trim().to_string(),
895 args.repo.trim().to_string(),
896 &handle.pull,
897 )
898 .await
899 {
900 Ok(result) => tool_ok(&result),
901 Err(error) => tool_error(error),
902 }
903}
904
905pub(crate) async fn gate(
906 services: &McpServices,
907 principal: &Principal,
908 args: args::PullReviewsArgs,
909) -> CallToolResult {
910 let handle = match handle_for(
911 services,
912 principal,
913 &args.space,
914 &args.repo,
915 &args.pull,
916 false,
917 )
918 .await
919 {
920 Ok(handle) => handle,
921 Err(error) => return error,
922 };
923 match services
924 .pulls
925 .pull_gate(
926 args.space.trim().to_string(),
927 args.repo.trim().to_string(),
928 &handle.ctx,
929 &handle.pull,
930 Some(principal.uid),
931 handle.admin,
932 )
933 .await
934 {
935 Ok(result) => tool_ok(&result),
936 Err(error) => tool_error(error),
937 }
938}
939
940pub(crate) async fn inbox(
941 services: &McpServices,
942 principal: &Principal,
943 args: args::PullInboxArgs,
944) -> CallToolResult {
945 let scope = match parse_enum::<InboxScope>(&args.scope) {
946 Ok(scope) => scope,
947 Err(error) => return error,
948 };
949 let query = InboxQuery {
950 state: args.state,
951 limit: args.limit,
952 offset: args.offset,
953 };
954 match services.pulls.list_inbox(principal.uid, scope, query).await {
955 Ok(result) => tool_ok(&result),
956 Err(error) => tool_error(error),
957 }
958}
Added lib/mcp-api/src/tools/repo.rs View lib/mcp-api/src/tools/repo.rs
@@ -0,0 +1,833 @@
1use std::path::PathBuf;
2
3use base64::Engine as _;
4use git::service::branches::CreateBranchCmd;
5use git::service::commits::ListCommitsCmd;
6use git::service::releases::{CreateReleaseCmd, ListReleasesQuery, UpdateReleaseCmd};
7use git::service::repos::{CreateRepoCmd, UpdateRepoCmd};
8use git::service::tags::CreateTagCmd;
9use git::service::watches::UpsertWatchCmd;
10use gitbaby::commit::types::TreeEntryMode;
11use models::common::watch_mode::WatchMode;
12use rmcp::model::CallToolResult;
13use serde_json::json;
14
15use crate::auth::Principal;
16use crate::error::{tool_done, tool_error, tool_ok, write_denied};
17use crate::services::McpServices;
18use crate::tools::args;
19use crate::tools::helpers::{page, parse_enum, repo_read, repo_write};
20
21pub(crate) async fn list(
22 services: &McpServices,
23 principal: &Principal,
24 args: args::RepoListArgs,
25) -> CallToolResult {
26 match services
27 .git
28 .list_repos(
29 args.space.trim().to_string(),
30 Some(principal.uid),
31 page(args.limit, args.offset, args.sort),
32 )
33 .await
34 {
35 Ok(result) => tool_ok(&result),
36 Err(error) => tool_error(error),
37 }
38}
39
40pub(crate) async fn get(
41 services: &McpServices,
42 principal: &Principal,
43 args: args::RepoRefArgs,
44) -> CallToolResult {
45 if let Err(error) = repo_read(services, principal.uid, &args.space, &args.repo).await {
46 return error;
47 }
48 match services
49 .git
50 .get_repo(args.space.trim().to_string(), args.repo.trim().to_string())
51 .await
52 {
53 Ok(result) => tool_ok(&result),
54 Err(error) => tool_error(error),
55 }
56}
57
58pub(crate) async fn create(
59 services: &McpServices,
60 principal: &Principal,
61 args: args::RepoCreateArgs,
62) -> CallToolResult {
63 if !principal.can_write() {
64 return write_denied();
65 }
66 let cmd = CreateRepoCmd {
67 name: args.name,
68 description: args.description,
69 is_private: args.is_private,
70 default_branch: args.default_branch,
71 parent_dir_id: args.parent_dir_id,
72 enable_lfs: args.enable_lfs,
73 license_name: args.license_name,
74 init_message: args.init_message,
75 };
76 match services
77 .git
78 .create_repo(args.space.trim().to_string(), principal.uid, cmd)
79 .await
80 {
81 Ok(result) => tool_ok(&result),
82 Err(error) => tool_error(error),
83 }
84}
85
86pub(crate) async fn update(
87 services: &McpServices,
88 principal: &Principal,
89 args: args::RepoUpdateArgs,
90) -> CallToolResult {
91 if !principal.can_write() {
92 return write_denied();
93 }
94 let cmd = UpdateRepoCmd {
95 description: args.description,
96 website: args.website,
97 is_private: args.is_private,
98 enable_lfs: args.enable_lfs,
99 default_branch: args.default_branch,
100 ai_review_on_push: args.ai_review_on_push,
101 ai_summary_on_push: args.ai_summary_on_push,
102 ai_review_on_pull: args.ai_review_on_pull,
103 };
104 match services
105 .git
106 .update_repo(
107 args.space.trim().to_string(),
108 args.repo.trim().to_string(),
109 principal.uid,
110 cmd,
111 )
112 .await
113 {
114 Ok(result) => tool_ok(&result),
115 Err(error) => tool_error(error),
116 }
117}
118
119pub(crate) async fn set_archived(
120 services: &McpServices,
121 principal: &Principal,
122 args: args::RepoArchiveArgs,
123) -> CallToolResult {
124 if !principal.can_write() {
125 return write_denied();
126 }
127 match services
128 .git
129 .set_repo_archived(
130 args.space.trim().to_string(),
131 args.repo.trim().to_string(),
132 principal.uid,
133 args.archived,
134 )
135 .await
136 {
137 Ok(result) => tool_ok(&result),
138 Err(error) => tool_error(error),
139 }
140}
141
142pub(crate) async fn branch_list(
143 services: &McpServices,
144 principal: &Principal,
145 args: args::RepoRefArgs,
146) -> CallToolResult {
147 if let Err(error) = repo_read(services, principal.uid, &args.space, &args.repo).await {
148 return error;
149 }
150 match services
151 .git
152 .list_branches(args.space.trim().to_string(), args.repo.trim().to_string())
153 .await
154 {
155 Ok(result) => tool_ok(&result),
156 Err(error) => tool_error(error),
157 }
158}
159
160pub(crate) async fn branch_get(
161 services: &McpServices,
162 principal: &Principal,
163 args: args::RepoBranchGetArgs,
164) -> CallToolResult {
165 if let Err(error) = repo_read(services, principal.uid, &args.space, &args.repo).await {
166 return error;
167 }
168 match services
169 .git
170 .get_branch(
171 args.space.trim().to_string(),
172 args.repo.trim().to_string(),
173 args.branch,
174 )
175 .await
176 {
177 Ok(result) => tool_ok(&result),
178 Err(error) => tool_error(error),
179 }
180}
181
182pub(crate) async fn branch_create(
183 services: &McpServices,
184 principal: &Principal,
185 args: args::RepoBranchCreateArgs,
186) -> CallToolResult {
187 if !principal.can_write() {
188 return write_denied();
189 }
190 if let Err(error) = repo_write(services, principal.uid, &args.space, &args.repo).await {
191 return error;
192 }
193 let cmd = CreateBranchCmd {
194 name: args.name,
195 start_point: args.start_point,
196 };
197 match services
198 .git
199 .create_branch(
200 args.space.trim().to_string(),
201 args.repo.trim().to_string(),
202 cmd,
203 )
204 .await
205 {
206 Ok(result) => tool_ok(&result),
207 Err(error) => tool_error(error),
208 }
209}
210
211pub(crate) async fn branch_rename(
212 services: &McpServices,
213 principal: &Principal,
214 args: args::RepoBranchRenameArgs,
215) -> CallToolResult {
216 if !principal.can_write() {
217 return write_denied();
218 }
219 if let Err(error) = repo_write(services, principal.uid, &args.space, &args.repo).await {
220 return error;
221 }
222 match services
223 .git
224 .rename_branch(
225 args.space.trim().to_string(),
226 args.repo.trim().to_string(),
227 args.from,
228 args.to,
229 )
230 .await
231 {
232 Ok(()) => tool_done(),
233 Err(error) => tool_error(error),
234 }
235}
236
237pub(crate) async fn branch_delete(
238 services: &McpServices,
239 principal: &Principal,
240 args: args::RepoBranchDeleteArgs,
241) -> CallToolResult {
242 if !principal.can_write() {
243 return write_denied();
244 }
245 if let Err(error) = repo_write(services, principal.uid, &args.space, &args.repo).await {
246 return error;
247 }
248 match services
249 .git
250 .delete_branch(
251 args.space.trim().to_string(),
252 args.repo.trim().to_string(),
253 args.name,
254 args.force,
255 )
256 .await
257 {
258 Ok(()) => tool_done(),
259 Err(error) => tool_error(error),
260 }
261}
262
263pub(crate) async fn branch_set_default(
264 services: &McpServices,
265 principal: &Principal,
266 args: args::RepoBranchDefaultArgs,
267) -> CallToolResult {
268 if !principal.can_write() {
269 return write_denied();
270 }
271 if let Err(error) = repo_write(services, principal.uid, &args.space, &args.repo).await {
272 return error;
273 }
274 match services
275 .git
276 .set_default_branch(
277 args.space.trim().to_string(),
278 args.repo.trim().to_string(),
279 args.name,
280 )
281 .await
282 {
283 Ok(()) => tool_done(),
284 Err(error) => tool_error(error),
285 }
286}
287
288pub(crate) async fn tag_list(
289 services: &McpServices,
290 principal: &Principal,
291 args: args::RepoRefArgs,
292) -> CallToolResult {
293 if let Err(error) = repo_read(services, principal.uid, &args.space, &args.repo).await {
294 return error;
295 }
296 match services
297 .git
298 .list_tags(args.space.trim().to_string(), args.repo.trim().to_string())
299 .await
300 {
301 Ok(result) => tool_ok(&result),
302 Err(error) => tool_error(error),
303 }
304}
305
306pub(crate) async fn tag_get(
307 services: &McpServices,
308 principal: &Principal,
309 args: args::RepoTagGetArgs,
310) -> CallToolResult {
311 if let Err(error) = repo_read(services, principal.uid, &args.space, &args.repo).await {
312 return error;
313 }
314 match services
315 .git
316 .get_tag(
317 args.space.trim().to_string(),
318 args.repo.trim().to_string(),
319 args.tag,
320 )
321 .await
322 {
323 Ok(result) => tool_ok(&result),
324 Err(error) => tool_error(error),
325 }
326}
327
328pub(crate) async fn tag_create(
329 services: &McpServices,
330 principal: &Principal,
331 args: args::RepoTagCreateArgs,
332) -> CallToolResult {
333 if !principal.can_write() {
334 return write_denied();
335 }
336 if let Err(error) = repo_write(services, principal.uid, &args.space, &args.repo).await {
337 return error;
338 }
339 let cmd = CreateTagCmd {
340 name: args.name,
341 target: args.target,
342 message: args.message,
343 annotated: args.annotated,
344 };
345 match services
346 .git
347 .create_tag(
348 args.space.trim().to_string(),
349 args.repo.trim().to_string(),
350 cmd,
351 )
352 .await
353 {
354 Ok(result) => tool_ok(&result),
355 Err(error) => tool_error(error),
356 }
357}
358
359pub(crate) async fn tag_delete(
360 services: &McpServices,
361 principal: &Principal,
362 args: args::RepoTagDeleteArgs,
363) -> CallToolResult {
364 if !principal.can_write() {
365 return write_denied();
366 }
367 if let Err(error) = repo_write(services, principal.uid, &args.space, &args.repo).await {
368 return error;
369 }
370 match services
371 .git
372 .delete_tag(
373 args.space.trim().to_string(),
374 args.repo.trim().to_string(),
375 args.name,
376 )
377 .await
378 {
379 Ok(()) => tool_done(),
380 Err(error) => tool_error(error),
381 }
382}
383
384pub(crate) async fn commit_list(
385 services: &McpServices,
386 principal: &Principal,
387 args: args::RepoCommitListArgs,
388) -> CallToolResult {
389 if let Err(error) = repo_read(services, principal.uid, &args.space, &args.repo).await {
390 return error;
391 }
392 let cmd = ListCommitsCmd {
393 revision: args.revision.unwrap_or_else(|| "HEAD".to_string()),
394 path: args.path.map(PathBuf::from),
395 max_count: args.max_count,
396 skip: args.skip,
397 since: None,
398 until: None,
399 first_parent: args.first_parent,
400 };
401 match services
402 .git
403 .list_commits(
404 args.space.trim().to_string(),
405 args.repo.trim().to_string(),
406 cmd,
407 Some(principal.uid),
408 )
409 .await
410 {
411 Ok(result) => tool_ok(&result),
412 Err(error) => tool_error(error),
413 }
414}
415
416pub(crate) async fn commit_get(
417 services: &McpServices,
418 principal: &Principal,
419 args: args::RepoCommitGetArgs,
420) -> CallToolResult {
421 if let Err(error) = repo_read(services, principal.uid, &args.space, &args.repo).await {
422 return error;
423 }
424 match services
425 .git
426 .get_commit(
427 args.space.trim().to_string(),
428 args.repo.trim().to_string(),
429 args.revision,
430 )
431 .await
432 {
433 Ok(result) => tool_ok(&result),
434 Err(error) => tool_error(error),
435 }
436}
437
438pub(crate) async fn tree(
439 services: &McpServices,
440 principal: &Principal,
441 args: args::RepoTreeArgs,
442) -> CallToolResult {
443 if let Err(error) = repo_read(services, principal.uid, &args.space, &args.repo).await {
444 return error;
445 }
446 let revision = args.revision.unwrap_or_else(|| "HEAD".to_string());
447 let space = args.space.trim().to_string();
448 let repo = args.repo.trim().to_string();
449 let result = if args.recurse {
450 services
451 .git
452 .tree_walk(space, repo, revision, true, true)
453 .await
454 } else {
455 services
456 .git
457 .tree_read(
458 space,
459 repo,
460 revision,
461 PathBuf::from(args.path.unwrap_or_default()),
462 )
463 .await
464 };
465 match result {
466 Ok(entries) => tool_ok(&entries),
467 Err(error) => tool_error(error),
468 }
469}
470
471pub(crate) async fn file_get(
472 services: &McpServices,
473 principal: &Principal,
474 args: args::RepoFileArgs,
475) -> CallToolResult {
476 if let Err(error) = repo_read(services, principal.uid, &args.space, &args.repo).await {
477 return error;
478 }
479 let revision = args.revision.unwrap_or_else(|| "HEAD".to_string());
480 let space = args.space.trim().to_string();
481 let repo = args.repo.trim().to_string();
482 let entry = match services
483 .git
484 .tree_resolve_path(space.clone(), repo.clone(), revision, args.path.clone())
485 .await
486 {
487 Ok(entry) => entry,
488 Err(error) => return tool_error(error),
489 };
490 if entry.mode == TreeEntryMode::Tree {
491 return crate::error::tool_message("目标路径是目录,请使用 repo_tree 工具");
492 }
493 let blob = match services
494 .git
495 .blob_content(space, repo, entry.oid.to_string())
496 .await
497 {
498 Ok(blob) => blob,
499 Err(error) => return tool_error(error),
500 };
501 let text = String::from_utf8(blob.data.clone()).ok();
502 let content = match &text {
503 Some(text) => json!({ "encoding": "utf-8", "text": text }),
504 None => json!({
505 "encoding": "base64",
506 "base64": base64::engine::general_purpose::STANDARD.encode(&blob.data),
507 }),
508 };
509 tool_ok(&json!({
510 "path": args.path,
511 "oid": blob.oid,
512 "size": blob.size,
513 "is_binary": blob.is_binary,
514 "truncated": blob.truncated,
515 "content": content,
516 }))
517}
518
519pub(crate) async fn release_list(
520 services: &McpServices,
521 principal: &Principal,
522 args: args::RepoReleaseListArgs,
523) -> CallToolResult {
524 if let Err(error) = repo_read(services, principal.uid, &args.space, &args.repo).await {
525 return error;
526 }
527 let query = ListReleasesQuery {
528 limit: args.limit,
529 offset: args.offset,
530 draft: args.draft,
531 };
532 match services
533 .git
534 .list_releases(
535 args.space.trim().to_string(),
536 args.repo.trim().to_string(),
537 query,
538 )
539 .await
540 {
541 Ok(result) => tool_ok(&result),
542 Err(error) => tool_error(error),
543 }
544}
545
546pub(crate) async fn release_get(
547 services: &McpServices,
548 principal: &Principal,
549 args: args::RepoReleaseGetArgs,
550) -> CallToolResult {
551 if let Err(error) = repo_read(services, principal.uid, &args.space, &args.repo).await {
552 return error;
553 }
554 match services
555 .git
556 .get_release(
557 args.space.trim().to_string(),
558 args.repo.trim().to_string(),
559 args.tag,
560 )
561 .await
562 {
563 Ok(result) => tool_ok(&result),
564 Err(error) => tool_error(error),
565 }
566}
567
568pub(crate) async fn release_create(
569 services: &McpServices,
570 principal: &Principal,
571 args: args::RepoReleaseCreateArgs,
572) -> CallToolResult {
573 if !principal.can_write() {
574 return write_denied();
575 }
576 if let Err(error) = repo_write(services, principal.uid, &args.space, &args.repo).await {
577 return error;
578 }
579 let cmd = CreateReleaseCmd {
580 tag_name: args.tag_name,
581 target: args.target,
582 title: args.title,
583 content: args.content,
584 is_draft: args.is_draft,
585 is_prerelease: args.is_prerelease,
586 };
587 match services
588 .git
589 .create_release(
590 args.space.trim().to_string(),
591 args.repo.trim().to_string(),
592 principal.uid,
593 cmd,
594 )
595 .await
596 {
597 Ok(result) => tool_ok(&result),
598 Err(error) => tool_error(error),
599 }
600}
601
602pub(crate) async fn release_update(
603 services: &McpServices,
604 principal: &Principal,
605 args: args::RepoReleaseUpdateArgs,
606) -> CallToolResult {
607 if !principal.can_write() {
608 return write_denied();
609 }
610 let ctx = match repo_write(services, principal.uid, &args.space, &args.repo).await {
611 Ok(ctx) => ctx,
612 Err(error) => return error,
613 };
614 let admin =
615 git::share::access::authorize_repo_access(&services.git, principal.uid, &ctx, true, true)
616 .await
617 .is_ok();
618 let cmd = UpdateReleaseCmd {
619 title: args.title,
620 content: args.content,
621 is_draft: args.is_draft,
622 is_prerelease: args.is_prerelease,
623 };
624 match services
625 .git
626 .update_release(
627 args.space.trim().to_string(),
628 args.repo.trim().to_string(),
629 args.tag,
630 principal.uid,
631 admin,
632 cmd,
633 )
634 .await
635 {
636 Ok(result) => tool_ok(&result),
637 Err(error) => tool_error(error),
638 }
639}
640
641pub(crate) async fn release_delete(
642 services: &McpServices,
643 principal: &Principal,
644 args: args::RepoReleaseDeleteArgs,
645) -> CallToolResult {
646 if !principal.can_write() {
647 return write_denied();
648 }
649 let ctx = match repo_write(services, principal.uid, &args.space, &args.repo).await {
650 Ok(ctx) => ctx,
651 Err(error) => return error,
652 };
653 let admin =
654 git::share::access::authorize_repo_access(&services.git, principal.uid, &ctx, true, true)
655 .await
656 .is_ok();
657 match services
658 .git
659 .delete_release(
660 args.space.trim().to_string(),
661 args.repo.trim().to_string(),
662 args.tag,
663 principal.uid,
664 admin,
665 )
666 .await
667 {
668 Ok(()) => tool_done(),
669 Err(error) => tool_error(error),
670 }
671}
672
673pub(crate) async fn topic_list(
674 services: &McpServices,
675 principal: &Principal,
676 args: args::RepoTopicArgs,
677) -> CallToolResult {
678 let ctx = match repo_read(services, principal.uid, &args.space, &args.repo).await {
679 Ok(ctx) => ctx,
680 Err(error) => return error,
681 };
682 match services.git.list_repo_topics(&ctx).await {
683 Ok(result) => tool_ok(&result),
684 Err(error) => tool_error(error),
685 }
686}
687
688pub(crate) async fn topic_add(
689 services: &McpServices,
690 principal: &Principal,
691 args: args::RepoTopicChangeArgs,
692) -> CallToolResult {
693 if !principal.can_write() {
694 return write_denied();
695 }
696 let ctx = match repo_write(services, principal.uid, &args.space, &args.repo).await {
697 Ok(ctx) => ctx,
698 Err(error) => return error,
699 };
700 match services
701 .git
702 .add_topic_to_repo(&ctx, args.topic.trim())
703 .await
704 {
705 Ok(result) => tool_ok(&result),
706 Err(error) => tool_error(error),
707 }
708}
709
710pub(crate) async fn topic_remove(
711 services: &McpServices,
712 principal: &Principal,
713 args: args::RepoTopicChangeArgs,
714) -> CallToolResult {
715 if !principal.can_write() {
716 return write_denied();
717 }
718 let ctx = match repo_write(services, principal.uid, &args.space, &args.repo).await {
719 Ok(ctx) => ctx,
720 Err(error) => return error,
721 };
722 match services
723 .git
724 .remove_topic_from_repo(&ctx, args.topic.trim())
725 .await
726 {
727 Ok(()) => tool_done(),
728 Err(error) => tool_error(error),
729 }
730}
731
732pub(crate) async fn star(
733 services: &McpServices,
734 principal: &Principal,
735 args: args::RepoRefArgs,
736) -> CallToolResult {
737 if !principal.can_write() {
738 return write_denied();
739 }
740 let ctx = match repo_read(services, principal.uid, &args.space, &args.repo).await {
741 Ok(ctx) => ctx,
742 Err(error) => return error,
743 };
744 match services.git.star_repo(&ctx, principal.uid).await {
745 Ok(result) => tool_ok(&result),
746 Err(error) => tool_error(error),
747 }
748}
749
750pub(crate) async fn unstar(
751 services: &McpServices,
752 principal: &Principal,
753 args: args::RepoRefArgs,
754) -> CallToolResult {
755 if !principal.can_write() {
756 return write_denied();
757 }
758 let ctx = match repo_read(services, principal.uid, &args.space, &args.repo).await {
759 Ok(ctx) => ctx,
760 Err(error) => return error,
761 };
762 match services.git.unstar_repo(&ctx, principal.uid).await {
763 Ok(result) => tool_ok(&result),
764 Err(error) => tool_error(error),
765 }
766}
767
768pub(crate) async fn watch(
769 services: &McpServices,
770 principal: &Principal,
771 args: args::RepoWatchArgs,
772) -> CallToolResult {
773 if !principal.can_write() {
774 return write_denied();
775 }
776 let mode = match parse_enum::<WatchMode>(&args.mode) {
777 Ok(mode) => mode,
778 Err(error) => return error,
779 };
780 let ctx = match repo_read(services, principal.uid, &args.space, &args.repo).await {
781 Ok(ctx) => ctx,
782 Err(error) => return error,
783 };
784 match services
785 .git
786 .upsert_watch(&ctx, principal.uid, UpsertWatchCmd { mode })
787 .await
788 {
789 Ok(result) => tool_ok(&result),
790 Err(error) => tool_error(error),
791 }
792}
793
794pub(crate) async fn stargazers(
795 services: &McpServices,
796 principal: &Principal,
797 args: args::RepoStargazerListArgs,
798) -> CallToolResult {
799 let ctx = match repo_read(services, principal.uid, &args.space, &args.repo).await {
800 Ok(ctx) => ctx,
801 Err(error) => return error,
802 };
803 let limit = args.limit.unwrap_or(50).clamp(1, 100);
804 let offset = args.offset.unwrap_or(0);
805 match services.git.list_stargazers(&ctx, limit, offset).await {
806 Ok(result) => tool_ok(&result),
807 Err(error) => tool_error(error),
808 }
809}
810
811pub(crate) async fn watchers(
812 services: &McpServices,
813 principal: &Principal,
814 args: args::RepoWatcherListArgs,
815) -> CallToolResult {
816 let mode = match args.mode.as_deref() {
817 Some(raw) => match parse_enum::<WatchMode>(raw) {
818 Ok(mode) => Some(mode),
819 Err(error) => return error,
820 },
821 None => None,
822 };
823 let ctx = match repo_read(services, principal.uid, &args.space, &args.repo).await {
824 Ok(ctx) => ctx,
825 Err(error) => return error,
826 };
827 let limit = args.limit.unwrap_or(50).clamp(1, 100);
828 let offset = args.offset.unwrap_or(0);
829 match services.git.list_watchers(&ctx, mode, limit, offset).await {
830 Ok(result) => tool_ok(&result),
831 Err(error) => tool_error(error),
832 }
833}
Added lib/mcp-api/src/tools/space.rs View lib/mcp-api/src/tools/space.rs
@@ -0,0 +1,646 @@
1use models::space::space_join_request::SpaceJoinRequestStatus;
2use rmcp::model::CallToolResult;
3use space::services::dir::{CreateDirCmd, MoveDirCmd, UpdateDirCmd};
4use space::services::groups::{AssignGroupCmd, CreateGroupCmd, UpdateGroupCmd};
5use space::services::invites::CreateInviteCmd;
6use space::services::join_requests::{CreateJoinRequestCmd, ExploreQuery, ListJoinRequestsQuery};
7use space::services::members::{AddMemberCmd, UpdateMemberCmd};
8use space::services::spaces::{CreateSpaceCmd, UpdateSpaceCmd};
9
10use crate::auth::Principal;
11use crate::error::{tool_done, tool_error, tool_ok, write_denied};
12use crate::services::McpServices;
13use crate::tools::args;
14use crate::tools::helpers::{page, parse_enum, parse_time};
15
16pub(crate) async fn list(
17 services: &McpServices,
18 principal: &Principal,
19 args: args::SpaceListArgs,
20) -> CallToolResult {
21 match services
22 .space
23 .list_spaces(principal.uid, page(args.limit, args.offset, None))
24 .await
25 {
26 Ok(result) => tool_ok(&result),
27 Err(error) => tool_error(error),
28 }
29}
30
31pub(crate) async fn explore(
32 services: &McpServices,
33 principal: &Principal,
34 args: args::SpaceExploreArgs,
35) -> CallToolResult {
36 let query = ExploreQuery {
37 q: args.q,
38 sort: args.sort,
39 limit: args.limit,
40 offset: args.offset,
41 };
42 match services
43 .space
44 .explore_spaces(Some(principal.uid), query)
45 .await
46 {
47 Ok(result) => tool_ok(&result),
48 Err(error) => tool_error(error),
49 }
50}
51
52pub(crate) async fn get(
53 services: &McpServices,
54 principal: &Principal,
55 args: args::SpaceNameArgs,
56) -> CallToolResult {
57 match services
58 .space
59 .get_space(Some(principal.uid), args.space.trim())
60 .await
61 {
62 Ok(result) => tool_ok(&result),
63 Err(error) => tool_error(error),
64 }
65}
66
67pub(crate) async fn create(
68 services: &McpServices,
69 principal: &Principal,
70 args: args::SpaceCreateArgs,
71) -> CallToolResult {
72 if !principal.can_write() {
73 return write_denied();
74 }
75 let allow_join_level = match args.allow_join_level.as_deref() {
76 Some(raw) => match parse_enum(raw) {
77 Ok(level) => Some(level),
78 Err(error) => return error,
79 },
80 None => None,
81 };
82 let cmd = CreateSpaceCmd {
83 name: args.name,
84 description: args.description,
85 avatar_url: None,
86 is_private: args.is_private,
87 allow_join: args.allow_join.unwrap_or(true),
88 allow_invite: args.allow_invite.unwrap_or(true),
89 welcome_message: args.welcome_message,
90 allow_join_level,
91 member_must_enable_2fa: args.member_must_enable_2fa,
92 welcome_object: None,
93 };
94 match services.space.create_space(principal.uid, cmd).await {
95 Ok(result) => tool_ok(&result),
96 Err(error) => tool_error(error),
97 }
98}
99
100pub(crate) async fn update(
101 services: &McpServices,
102 principal: &Principal,
103 args: args::SpaceUpdateArgs,
104) -> CallToolResult {
105 if !principal.can_write() {
106 return write_denied();
107 }
108 let allow_join_level = match args.allow_join_level.as_deref() {
109 Some(raw) => match parse_enum(raw) {
110 Ok(level) => Some(level),
111 Err(error) => return error,
112 },
113 None => None,
114 };
115 let cmd = UpdateSpaceCmd {
116 name: args.name,
117 description: args.description.map(Some),
118 is_private: args.is_private,
119 allow_join: args.allow_join,
120 allow_invite: args.allow_invite,
121 welcome_message: args.welcome_message.map(Some),
122 allow_join_level,
123 member_must_enable_2fa: args.member_must_enable_2fa,
124 welcome_object: None,
125 };
126 match services
127 .space
128 .update_space(principal.uid, args.space.trim(), cmd)
129 .await
130 {
131 Ok(result) => tool_ok(&result),
132 Err(error) => tool_error(error),
133 }
134}
135
136pub(crate) async fn delete(
137 services: &McpServices,
138 principal: &Principal,
139 args: args::SpaceNameArgs,
140) -> CallToolResult {
141 if !principal.can_write() {
142 return write_denied();
143 }
144 match services
145 .space
146 .delete_space(principal.uid, args.space.trim())
147 .await
148 {
149 Ok(()) => tool_done(),
150 Err(error) => tool_error(error),
151 }
152}
153
154pub(crate) async fn member_list(
155 services: &McpServices,
156 principal: &Principal,
157 args: args::SpaceMemberListArgs,
158) -> CallToolResult {
159 match services
160 .space
161 .list_members(
162 principal.uid,
163 args.space.trim(),
164 page(args.limit, args.offset, None),
165 )
166 .await
167 {
168 Ok(result) => tool_ok(&result),
169 Err(error) => tool_error(error),
170 }
171}
172
173pub(crate) async fn member_add(
174 services: &McpServices,
175 principal: &Principal,
176 args: args::SpaceMemberAddArgs,
177) -> CallToolResult {
178 if !principal.can_write() {
179 return write_denied();
180 }
181 let cmd = AddMemberCmd {
182 user_id: args.user_id,
183 alias_name: args.alias_name,
184 alias_avatar_url: None,
185 message_notifications: None,
186 muted: None,
187 mute_until: None,
188 mobile_push: None,
189 suppress_everyone: None,
190 suppress_roles: None,
191 hide_muted_channels: None,
192 };
193 match services
194 .space
195 .add_member(principal.uid, args.space.trim(), cmd)
196 .await
197 {
198 Ok(result) => tool_ok(&result),
199 Err(error) => tool_error(error),
200 }
201}
202
203pub(crate) async fn member_update(
204 services: &McpServices,
205 principal: &Principal,
206 args: args::SpaceMemberUpdateArgs,
207) -> CallToolResult {
208 if !principal.can_write() {
209 return write_denied();
210 }
211 let cmd = UpdateMemberCmd {
212 alias_name: args.alias_name.map(Some),
213 alias_avatar_url: None,
214 message_notifications: None,
215 muted: args.muted,
216 mute_until: None,
217 mobile_push: None,
218 suppress_everyone: None,
219 suppress_roles: None,
220 hide_muted_channels: None,
221 };
222 match services
223 .space
224 .update_member(principal.uid, args.space.trim(), args.user_id, cmd)
225 .await
226 {
227 Ok(result) => tool_ok(&result),
228 Err(error) => tool_error(error),
229 }
230}
231
232pub(crate) async fn member_remove(
233 services: &McpServices,
234 principal: &Principal,
235 args: args::SpaceMemberRefArgs,
236) -> CallToolResult {
237 if !principal.can_write() {
238 return write_denied();
239 }
240 match services
241 .space
242 .remove_member(principal.uid, args.space.trim(), args.user_id)
243 .await
244 {
245 Ok(()) => tool_done(),
246 Err(error) => tool_error(error),
247 }
248}
249
250pub(crate) async fn member_leave(
251 services: &McpServices,
252 principal: &Principal,
253 args: args::SpaceNameArgs,
254) -> CallToolResult {
255 if !principal.can_write() {
256 return write_denied();
257 }
258 match services
259 .space
260 .leave_space(principal.uid, args.space.trim())
261 .await
262 {
263 Ok(()) => tool_done(),
264 Err(error) => tool_error(error),
265 }
266}
267
268pub(crate) async fn group_list(
269 services: &McpServices,
270 principal: &Principal,
271 args: args::SpaceGroupListArgs,
272) -> CallToolResult {
273 match services
274 .space
275 .list_groups(
276 principal.uid,
277 args.space.trim(),
278 page(args.limit, args.offset, None),
279 )
280 .await
281 {
282 Ok(result) => tool_ok(&result),
283 Err(error) => tool_error(error),
284 }
285}
286
287pub(crate) async fn group_create(
288 services: &McpServices,
289 principal: &Principal,
290 args: args::SpaceGroupCreateArgs,
291) -> CallToolResult {
292 if !principal.can_write() {
293 return write_denied();
294 }
295 let cmd = CreateGroupCmd {
296 name: args.name,
297 color: args.color,
298 unicode_emoji: None,
299 icon_hash: None,
300 position: args.position,
301 permissions: args.permissions,
302 };
303 match services
304 .space
305 .create_group(principal.uid, args.space.trim(), cmd)
306 .await
307 {
308 Ok(result) => tool_ok(&result),
309 Err(error) => tool_error(error),
310 }
311}
312
313pub(crate) async fn group_update(
314 services: &McpServices,
315 principal: &Principal,
316 args: args::SpaceGroupUpdateArgs,
317) -> CallToolResult {
318 if !principal.can_write() {
319 return write_denied();
320 }
321 let cmd = UpdateGroupCmd {
322 name: args.name,
323 color: args.color,
324 unicode_emoji: None,
325 icon_hash: None,
326 position: args.position,
327 permissions: args.permissions,
328 };
329 match services
330 .space
331 .update_group(principal.uid, args.space.trim(), args.group_id, cmd)
332 .await
333 {
334 Ok(result) => tool_ok(&result),
335 Err(error) => tool_error(error),
336 }
337}
338
339pub(crate) async fn group_delete(
340 services: &McpServices,
341 principal: &Principal,
342 args: args::SpaceGroupRefArgs,
343) -> CallToolResult {
344 if !principal.can_write() {
345 return write_denied();
346 }
347 match services
348 .space
349 .delete_group(principal.uid, args.space.trim(), args.group_id)
350 .await
351 {
352 Ok(()) => tool_done(),
353 Err(error) => tool_error(error),
354 }
355}
356
357pub(crate) async fn group_assign(
358 services: &McpServices,
359 principal: &Principal,
360 args: args::SpaceGroupAssignArgs,
361) -> CallToolResult {
362 if !principal.can_write() {
363 return write_denied();
364 }
365 let cmd = AssignGroupCmd {
366 group_id: args.group_id,
367 };
368 match services
369 .space
370 .assign_group(principal.uid, args.space.trim(), args.user_id, cmd)
371 .await
372 {
373 Ok(()) => tool_done(),
374 Err(error) => tool_error(error),
375 }
376}
377
378pub(crate) async fn group_unassign(
379 services: &McpServices,
380 principal: &Principal,
381 args: args::SpaceGroupAssignArgs,
382) -> CallToolResult {
383 if !principal.can_write() {
384 return write_denied();
385 }
386 match services
387 .space
388 .unassign_group(
389 principal.uid,
390 args.space.trim(),
391 args.user_id,
392 args.group_id,
393 )
394 .await
395 {
396 Ok(()) => tool_done(),
397 Err(error) => tool_error(error),
398 }
399}
400
401pub(crate) async fn dir_tree(
402 services: &McpServices,
403 principal: &Principal,
404 args: args::SpaceDirTreeArgs,
405) -> CallToolResult {
406 match services
407 .space
408 .get_space_dir_tree(principal.uid, args.space.trim(), args.parent_id)
409 .await
410 {
411 Ok(result) => tool_ok(&result),
412 Err(error) => tool_error(error),
413 }
414}
415
416pub(crate) async fn dir_create(
417 services: &McpServices,
418 principal: &Principal,
419 args: args::SpaceDirCreateArgs,
420) -> CallToolResult {
421 if !principal.can_write() {
422 return write_denied();
423 }
424 let cmd = CreateDirCmd {
425 name: args.name,
426 parent_dir_id: args.parent_id,
427 position: None,
428 };
429 match services
430 .space
431 .create_dir_node(principal.uid, args.space.trim(), cmd)
432 .await
433 {
434 Ok(result) => tool_ok(&result),
435 Err(error) => tool_error(error),
436 }
437}
438
439pub(crate) async fn dir_update(
440 services: &McpServices,
441 principal: &Principal,
442 args: args::SpaceDirUpdateArgs,
443) -> CallToolResult {
444 if !principal.can_write() {
445 return write_denied();
446 }
447 let cmd = UpdateDirCmd { name: args.name };
448 match services
449 .space
450 .update_dir_node(principal.uid, args.space.trim(), args.dir_id, cmd)
451 .await
452 {
453 Ok(result) => tool_ok(&result),
454 Err(error) => tool_error(error),
455 }
456}
457
458pub(crate) async fn dir_move(
459 services: &McpServices,
460 principal: &Principal,
461 args: args::SpaceDirMoveArgs,
462) -> CallToolResult {
463 if !principal.can_write() {
464 return write_denied();
465 }
466 let cmd = MoveDirCmd {
467 parent_dir_id: args.parent_id.map(Some),
468 position: args.position,
469 };
470 match services
471 .space
472 .move_dir_node(principal.uid, args.space.trim(), args.dir_id, cmd)
473 .await
474 {
475 Ok(result) => tool_ok(&result),
476 Err(error) => tool_error(error),
477 }
478}
479
480pub(crate) async fn invite_list(
481 services: &McpServices,
482 principal: &Principal,
483 args: args::SpaceInviteListArgs,
484) -> CallToolResult {
485 match services
486 .space
487 .list_invites(
488 principal.uid,
489 args.space.trim(),
490 page(args.limit, args.offset, None),
491 )
492 .await
493 {
494 Ok(result) => tool_ok(&result),
495 Err(error) => tool_error(error),
496 }
497}
498
499pub(crate) async fn invite_create(
500 services: &McpServices,
501 principal: &Principal,
502 args: args::SpaceInviteCreateArgs,
503) -> CallToolResult {
504 if !principal.can_write() {
505 return write_denied();
506 }
507 let expires_at = match args.expires_at.as_deref() {
508 Some(raw) => match parse_time(raw) {
509 Ok(time) => Some(time),
510 Err(error) => return error,
511 },
512 None => None,
513 };
514 let cmd = CreateInviteCmd {
515 user_id: args.user_id,
516 email: args.email,
517 group_ids: args.group_ids,
518 expires_at,
519 };
520 match services
521 .space
522 .create_invite(principal.uid, args.space.trim(), cmd)
523 .await
524 {
525 Ok(result) => tool_ok(&result),
526 Err(error) => tool_error(error),
527 }
528}
529
530pub(crate) async fn invite_revoke(
531 services: &McpServices,
532 principal: &Principal,
533 args: args::SpaceInviteRefArgs,
534) -> CallToolResult {
535 if !principal.can_write() {
536 return write_denied();
537 }
538 match services
539 .space
540 .revoke_invite(principal.uid, args.space.trim(), args.invite_id)
541 .await
542 {
543 Ok(result) => tool_ok(&result),
544 Err(error) => tool_error(error),
545 }
546}
547
548pub(crate) async fn join_request_list(
549 services: &McpServices,
550 principal: &Principal,
551 args: args::SpaceJoinRequestListArgs,
552) -> CallToolResult {
553 let status = match args.status.as_deref() {
554 Some(raw) => match parse_enum::<SpaceJoinRequestStatus>(raw) {
555 Ok(status) => Some(status),
556 Err(error) => return error,
557 },
558 None => None,
559 };
560 let query = ListJoinRequestsQuery {
561 status,
562 limit: args.limit,
563 offset: args.offset,
564 };
565 match services
566 .space
567 .list_join_requests(principal.uid, args.space.trim(), query)
568 .await
569 {
570 Ok(result) => tool_ok(&result),
571 Err(error) => tool_error(error),
572 }
573}
574
575pub(crate) async fn join_request_create(
576 services: &McpServices,
577 principal: &Principal,
578 args: args::SpaceJoinRequestCreateArgs,
579) -> CallToolResult {
580 if !principal.can_write() {
581 return write_denied();
582 }
583 let cmd = CreateJoinRequestCmd { note: args.note };
584 match services
585 .space
586 .create_join_request(principal.uid, args.space.trim(), cmd)
587 .await
588 {
589 Ok(result) => tool_ok(&result),
590 Err(error) => tool_error(error),
591 }
592}
593
594pub(crate) async fn join_request_withdraw(
595 services: &McpServices,
596 principal: &Principal,
597 args: args::SpaceJoinRequestRefArgs,
598) -> CallToolResult {
599 if !principal.can_write() {
600 return write_denied();
601 }
602 match services
603 .space
604 .withdraw_join_request(principal.uid, args.space.trim(), args.request_id)
605 .await
606 {
607 Ok(()) => tool_done(),
608 Err(error) => tool_error(error),
609 }
610}
611
612pub(crate) async fn join_request_approve(
613 services: &McpServices,
614 principal: &Principal,
615 args: args::SpaceJoinRequestRefArgs,
616) -> CallToolResult {
617 if !principal.can_write() {
618 return write_denied();
619 }
620 match services
621 .space
622 .approve_join_request(principal.uid, args.space.trim(), args.request_id)
623 .await
624 {
625 Ok(result) => tool_ok(&result),
626 Err(error) => tool_error(error),
627 }
628}
629
630pub(crate) async fn join_request_reject(
631 services: &McpServices,
632 principal: &Principal,
633 args: args::SpaceJoinRequestRefArgs,
634) -> CallToolResult {
635 if !principal.can_write() {
636 return write_denied();
637 }
638 match services
639 .space
640 .reject_join_request(principal.uid, args.space.trim(), args.request_id)
641 .await
642 {
643 Ok(result) => tool_ok(&result),
644 Err(error) => tool_error(error),
645 }
646}
Added lib/mcp-api/src/auth.rs View lib/mcp-api/src/auth.rs
@@ -0,0 +1,140 @@
1use std::net::SocketAddr;
2
3use axum::Json;
4use axum::body::Body;
5use axum::extract::{ConnectInfo, State};
6use axum::http::{HeaderMap, HeaderValue, Request, StatusCode, header};
7use axum::middleware::Next;
8use axum::response::{IntoResponse, Response};
9use base64::Engine as _;
10use models::users::user_access_keys::AccessKeyScope;
11use rmcp::ErrorData;
12use rmcp::RoleServer;
13use rmcp::service::RequestContext;
14use serde_json::json;
15use uuid::Uuid;
16
17use crate::{McpRuntime, net};
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20pub enum PrincipalSource {
21 UserKey,
22 InternalKey,
23}
24
25#[derive(Debug, Clone)]
26pub struct Principal {
27 pub uid: Uuid,
28 pub scope: AccessKeyScope,
29 pub full_access: bool,
30 pub source: PrincipalSource,
31}
32
33impl Principal {
34 pub fn can_write(&self) -> bool {
35 self.full_access || self.scope.allows(true)
36 }
37
38 pub(crate) fn from_ctx(ctx: &RequestContext<RoleServer>) -> Result<Self, ErrorData> {
39 let parts = ctx
40 .extensions
41 .get::<axum::http::request::Parts>()
42 .ok_or_else(|| ErrorData::internal_error("missing http request parts", None))?;
43 parts
44 .extensions
45 .get::<Principal>()
46 .cloned()
47 .ok_or_else(|| ErrorData::invalid_request("unauthenticated", None))
48 }
49}
50
51pub(crate) async fn auth_middleware(
52 State(runtime): State<McpRuntime>,
53 ConnectInfo(peer): ConnectInfo<SocketAddr>,
54 mut request: Request<Body>,
55 next: Next,
56) -> Response {
57 let Some(token) = extract_token(request.headers()) else {
58 return unauthorized("缺少访问密钥");
59 };
60 let peer = Some(peer);
61 let client_ip = net::resolve_client_ip(peer, request.headers(), runtime.trusted_proxies());
62
63 let principal = if let Some(name) = runtime.match_internal_key(&token) {
64 if !runtime.is_internal_ip(client_ip) {
65 tracing::warn!(key = %name, "internal mcp key rejected from a non-internal source");
66 return forbidden("内部访问密钥仅允许内网来源使用");
67 }
68 let Some(uid) = runtime.system_uid() else {
69 return unavailable("内部系统账号尚未就绪");
70 };
71 Principal {
72 uid,
73 scope: AccessKeyScope::Write,
74 full_access: true,
75 source: PrincipalSource::InternalKey,
76 }
77 } else {
78 match runtime.services().auth.verify_access_token(&token).await {
79 Ok(Some(verified)) => Principal {
80 uid: verified.user_id,
81 scope: verified.scope,
82 full_access: false,
83 source: PrincipalSource::UserKey,
84 },
85 Ok(None) => return unauthorized("访问密钥无效或已过期"),
86 Err(error) => {
87 tracing::warn!(%error, "mcp access key verification failed");
88 return unavailable("鉴权服务暂不可用");
89 }
90 }
91 };
92
93 request.extensions_mut().insert(principal);
94 next.run(request).await
95}
96
97fn extract_token(headers: &HeaderMap) -> Option<String> {
98 let value = headers.get(header::AUTHORIZATION)?.to_str().ok()?;
99 if let Some(rest) = value
100 .strip_prefix("Bearer ")
101 .or_else(|| value.strip_prefix("bearer "))
102 {
103 let token = rest.trim();
104 return (!token.is_empty()).then(|| token.to_string());
105 }
106 if let Some(rest) = value.strip_prefix("Basic ") {
107 let decoded = base64::engine::general_purpose::STANDARD
108 .decode(rest.trim())
109 .ok()?;
110 let credentials = String::from_utf8(decoded).ok()?;
111 return credentials
112 .split_once(':')
113 .map(|(_, password)| password.to_string());
114 }
115 None
116}
117
118fn error_response(status: StatusCode, message: &str) -> Response {
119 let body = Json(json!({
120 "error": status.canonical_reason().unwrap_or("error").to_lowercase(),
121 "message": message,
122 }));
123 (status, body).into_response()
124}
125
126fn unauthorized(message: &str) -> Response {
127 let mut response = error_response(StatusCode::UNAUTHORIZED, message);
128 response
129 .headers_mut()
130 .insert(header::WWW_AUTHENTICATE, HeaderValue::from_static("Bearer"));
131 response
132}
133
134fn forbidden(message: &str) -> Response {
135 error_response(StatusCode::FORBIDDEN, message)
136}
137
138fn unavailable(message: &str) -> Response {
139 error_response(StatusCode::SERVICE_UNAVAILABLE, message)
140}
Added lib/mcp-api/src/catalog.rs View lib/mcp-api/src/catalog.rs
@@ -0,0 +1,38 @@
1use serde::Serialize;
2
3use crate::McpServer;
4
5#[derive(Debug, Clone, Serialize)]
6pub struct McpToolInfo {
7 pub name: String,
8 pub group: String,
9 pub description: String,
10 pub read_only: bool,
11}
12
13pub fn tools() -> Vec<McpToolInfo> {
14 let router = McpServer::tool_router();
15 let mut tools: Vec<McpToolInfo> = router
16 .list_all()
17 .into_iter()
18 .map(|tool| {
19 let name = tool.name.to_string();
20 let group = name.split('_').next().unwrap_or("misc").to_string();
21 McpToolInfo {
22 group,
23 read_only: tool
24 .annotations
25 .as_ref()
26 .and_then(|annotations| annotations.read_only_hint)
27 .unwrap_or(false),
28 description: tool
29 .description
30 .map(|description| description.to_string())
31 .unwrap_or_default(),
32 name,
33 }
34 })
35 .collect();
36 tools.sort_by(|a, b| a.name.cmp(&b.name));
37 tools
38}
Added lib/mcp-api/src/error.rs View lib/mcp-api/src/error.rs
@@ -0,0 +1,24 @@
1use rmcp::model::{CallToolResult, ContentBlock};
2
3pub fn tool_ok<T: serde::Serialize>(value: &T) -> CallToolResult {
4 match serde_json::to_string_pretty(value) {
5 Ok(text) => CallToolResult::success(vec![ContentBlock::text(text)]),
6 Err(error) => tool_message(format!("序列化结果失败: {error}")),
7 }
8}
9
10pub fn tool_done() -> CallToolResult {
11 tool_ok(&serde_json::json!({ "ok": true }))
12}
13
14pub fn tool_message(message: impl Into<String>) -> CallToolResult {
15 CallToolResult::error(vec![ContentBlock::text(message.into())])
16}
17
18pub fn tool_error(error: impl std::fmt::Display) -> CallToolResult {
19 tool_message(error.to_string())
20}
21
22pub fn write_denied() -> CallToolResult {
23 tool_message("当前访问密钥只有只读权限,无法执行写操作")
24}
Added lib/mcp-api/src/lib.rs View lib/mcp-api/src/lib.rs
@@ -0,0 +1,153 @@
1pub mod catalog;
2pub mod error;
3pub mod settings;
4
5mod auth;
6mod net;
7mod server;
8mod services;
9mod tools;
10
11pub use server::McpServer;
12pub use services::{McpServices, SharedServices};
13
14use std::net::IpAddr;
15use std::sync::Arc;
16
17use axum::Router;
18use axum::middleware;
19use export::AppState;
20use ipnet::IpNet;
21use rmcp::transport::StreamableHttpServerConfig;
22use rmcp::transport::StreamableHttpService;
23use rmcp::transport::streamable_http_server::session::local::LocalSessionManager;
24use sha2::{Digest, Sha256};
25use uuid::Uuid;
26
27struct InternalKey {
28 name: String,
29 hash: String,
30}
31
32pub(crate) struct RuntimeInner {
33 pub(crate) services: SharedServices,
34 internal_keys: Vec<InternalKey>,
35 internal_cidrs: Vec<IpNet>,
36 trusted_proxies: Vec<IpNet>,
37 system_uid: Option<Uuid>,
38}
39
40#[derive(Clone)]
41pub(crate) struct McpRuntime(Arc<RuntimeInner>);
42
43impl McpRuntime {
44 pub(crate) fn services(&self) -> &SharedServices {
45 &self.0.services
46 }
47
48 pub(crate) fn system_uid(&self) -> Option<Uuid> {
49 self.0.system_uid
50 }
51
52 pub(crate) fn trusted_proxies(&self) -> &[IpNet] {
53 &self.0.trusted_proxies
54 }
55
56 pub(crate) fn match_internal_key(&self, token: &str) -> Option<&str> {
57 let hash = sha256_hex(token.as_bytes());
58 self.0
59 .internal_keys
60 .iter()
61 .find(|key| constant_time_eq(hash.as_bytes(), key.hash.as_bytes()))
62 .map(|key| key.name.as_str())
63 }
64
65 pub(crate) fn is_internal_ip(&self, ip: Option<IpAddr>) -> bool {
66 ip.is_some_and(|ip| self.0.internal_cidrs.iter().any(|net| net.contains(&ip)))
67 }
68}
69
70pub async fn router(state: AppState) -> Router {
71 let config = state.config.mcp.clone();
72 if !config.enabled() {
73 return Router::new();
74 }
75
76 let system_uid = match agent::bot::resolve_bot_user(state.database.write(), &state.config.ai)
77 .await
78 {
79 Ok(uid) => {
80 export::authz::set_system_uid(uid);
81 Some(uid)
82 }
83 Err(error) => {
84 tracing::warn!(%error, "failed to resolve the platform bot user for mcp internal keys");
85 None
86 }
87 };
88
89 let internal_keys = config
90 .internal_keys
91 .iter()
92 .filter(|key| key.enabled())
93 .map(|key| InternalKey {
94 name: key.name.clone(),
95 hash: key
96 .token_hash
97 .as_ref()
98 .map(|hash| hash.trim().to_lowercase())
99 .unwrap_or_else(|| sha256_hex(key.token.as_deref().unwrap_or_default().as_bytes())),
100 })
101 .collect();
102
103 let runtime = McpRuntime(Arc::new(RuntimeInner {
104 services: Arc::new(McpServices::new(state)),
105 internal_keys,
106 internal_cidrs: config.parsed_internal_cidrs(),
107 trusted_proxies: config.parsed_trusted_proxies(),
108 system_uid,
109 }));
110
111 let mut http_config = StreamableHttpServerConfig::default()
112 .with_legacy_session_mode(false)
113 .with_json_response(true);
114 if !config.allowed_hosts.is_empty() {
115 http_config = http_config.with_allowed_hosts(config.allowed_hosts.clone());
116 }
117
118 let services = runtime.services().clone();
119 let mcp_service = StreamableHttpService::new(
120 move || Ok(McpServer::new(services.clone())),
121 LocalSessionManager::default().into(),
122 http_config,
123 );
124
125 tracing::info!(
126 internal_keys = runtime.0.internal_keys.len(),
127 internal_cidrs = runtime.0.internal_cidrs.len(),
128 "mcp endpoint ready"
129 );
130
131 Router::new()
132 .fallback_service(mcp_service)
133 .layer(middleware::from_fn_with_state(
134 runtime,
135 auth::auth_middleware,
136 ))
137}
138
139pub(crate) fn sha256_hex(data: &[u8]) -> String {
140 let digest = Sha256::digest(data);
141 digest.iter().map(|byte| format!("{byte:02x}")).collect()
142}
143
144fn constant_time_eq(a: &[u8], b: &[u8]) -> bool {
145 if a.len() != b.len() {
146 return false;
147 }
148 let mut diff: u8 = 0;
149 for (x, y) in a.iter().zip(b.iter()) {
150 diff |= x ^ y;
151 }
152 diff == 0
153}
Added lib/mcp-api/src/net.rs View lib/mcp-api/src/net.rs
@@ -0,0 +1,42 @@
1use std::net::{IpAddr, SocketAddr};
2
3use axum::http::HeaderMap;
4use ipnet::IpNet;
5
6pub fn resolve_client_ip(
7 peer: Option<SocketAddr>,
8 headers: &HeaderMap,
9 trusted_proxies: &[IpNet],
10) -> Option<IpAddr> {
11 match peer {
12 Some(addr) if in_nets(addr.ip(), trusted_proxies) => forwarded_ip(headers),
13 Some(addr) => Some(addr.ip()),
14 None => None,
15 }
16}
17
18pub fn in_nets(ip: IpAddr, nets: &[IpNet]) -> bool {
19 nets.iter().any(|net| net.contains(&ip))
20}
21
22fn forwarded_ip(headers: &HeaderMap) -> Option<IpAddr> {
23 if let Some(value) = headers
24 .get("x-forwarded-for")
25 .and_then(|value| value.to_str().ok())
26 && let Some(first) = value
27 .split(',')
28 .map(str::trim)
29 .find(|item| !item.is_empty())
30 && let Ok(ip) = first.parse::<IpAddr>()
31 {
32 return Some(ip);
33 }
34 if let Some(value) = headers
35 .get("x-real-ip")
36 .and_then(|value| value.to_str().ok())
37 && let Ok(ip) = value.trim().parse::<IpAddr>()
38 {
39 return Some(ip);
40 }
41 None
42}
Added lib/mcp-api/src/server.rs View lib/mcp-api/src/server.rs
@@ -0,0 +1,84 @@
1use rmcp::ErrorData;
2use rmcp::ServerHandler;
3use rmcp::handler::server::router::tool::ToolRouter;
4use rmcp::handler::server::tool::ToolCallContext;
5use rmcp::model::{
6 CallToolRequestParams, CallToolResponse, CallToolResult, ContentBlock, ListToolsResult,
7 PaginatedRequestParams,
8};
9use rmcp::service::RequestContext;
10use rmcp::{RoleServer, tool_handler};
11
12use crate::auth::Principal;
13use crate::services::SharedServices;
14use crate::settings;
15
16#[derive(Clone)]
17pub struct McpServer {
18 pub(crate) services: SharedServices,
19 pub(crate) tool_router: ToolRouter<Self>,
20}
21
22impl McpServer {
23 pub fn new(services: SharedServices) -> Self {
24 Self {
25 services,
26 tool_router: Self::tool_router(),
27 }
28 }
29
30 pub(crate) fn principal(
31 &self,
32 ctx: &RequestContext<RoleServer>,
33 ) -> Result<Principal, ErrorData> {
34 Principal::from_ctx(ctx)
35 }
36}
37
38#[tool_handler(
39 name = "gitdata-mcp",
40 version = "1.0.0",
41 instructions = "GitData 平台的 MCP 工具集。调用结果遵循调用者访问密钥的权限:只读密钥只能调用只读工具,空间/仓库内的操作受成员角色与仓库 ACL 限制。"
42)]
43impl ServerHandler for McpServer {
44 async fn list_tools(
45 &self,
46 _request: Option<PaginatedRequestParams>,
47 context: RequestContext<RoleServer>,
48 ) -> Result<ListToolsResult, ErrorData> {
49 let principal = self.principal(&context)?;
50 let disabled = settings::read_disabled(&self.services.state).await;
51 let mut tools = self.tool_router.list_all();
52 tools.retain(|tool| !disabled.contains(tool.name.as_ref()));
53 if !principal.can_write() {
54 tools.retain(|tool| {
55 tool.annotations
56 .as_ref()
57 .and_then(|annotations| annotations.read_only_hint)
58 .unwrap_or(false)
59 });
60 }
61 Ok(ListToolsResult {
62 tools,
63 ..Default::default()
64 })
65 }
66
67 async fn call_tool(
68 &self,
69 request: CallToolRequestParams,
70 context: RequestContext<RoleServer>,
71 ) -> Result<CallToolResponse, ErrorData> {
72 self.principal(&context)?;
73 let name = request.name.to_string();
74 let disabled = settings::read_disabled(&self.services.state).await;
75 if disabled.contains(&name) {
76 return Ok(CallToolResult::error(vec![ContentBlock::text(format!(
77 "工具 {name} 已被管理员停用"
78 ))])
79 .into());
80 }
81 let call = ToolCallContext::new(self, request, context);
82 self.tool_router.call(call).await
83 }
84}
Added lib/mcp-api/src/services.rs View lib/mcp-api/src/services.rs
@@ -0,0 +1,30 @@
1use std::sync::Arc;
2
3use export::AppState;
4
5#[derive(Clone)]
6pub struct McpServices {
7 pub state: AppState,
8 pub auth: auth::services::AuthService,
9 pub user: user::services::service::UserService,
10 pub space: space::services::service::SpaceService,
11 pub git: git::service::GitService,
12 pub issues: issues::services::service::IssueService,
13 pub pulls: pulls::services::service::PullService,
14}
15
16impl McpServices {
17 pub fn new(state: AppState) -> Self {
18 Self {
19 auth: auth::services::AuthService::new(state.clone()),
20 user: user::services::service::UserService::new(state.clone()),
21 space: space::services::service::SpaceService::new(state.clone()),
22 git: git::service::GitService::new(state.clone()),
23 issues: issues::services::service::IssueService::new(state.clone()),
24 pulls: pulls::services::service::PullService::new(state.clone()),
25 state,
26 }
27 }
28}
29
30pub type SharedServices = Arc<McpServices>;
Added lib/mcp-api/src/settings.rs View lib/mcp-api/src/settings.rs
@@ -0,0 +1,102 @@
1use std::collections::HashSet;
2use std::time::Duration;
3
4use cache::CacheClient;
5use export::AppState;
6use models::system::system_settings;
7use sea_orm::sea_query::OnConflict;
8use sea_orm::{DatabaseConnection, EntityTrait, Set};
9use serde::{Deserialize, Serialize};
10
11pub const DB_KEY: &str = "mcp.tools";
12pub const CACHE_KEY: &str = "system:setting:mcp.tools";
13const CACHE_TTL: Duration = Duration::from_secs(30);
14
15#[derive(Debug, Clone, Default, Serialize, Deserialize)]
16pub struct McpToolSettings {
17 #[serde(default)]
18 pub disabled: Vec<String>,
19 #[serde(default)]
20 pub updated_by: Option<String>,
21 #[serde(default)]
22 pub updated_at: Option<String>,
23}
24
25pub async fn read(state: &AppState) -> McpToolSettings {
26 let cache = CacheClient::new(state.redis.clone());
27 if let Ok(Some(settings)) = cache.get::<McpToolSettings>(CACHE_KEY).await {
28 return settings;
29 }
30 match system_settings::Entity::find_by_id(DB_KEY.to_string())
31 .one(state.database.read())
32 .await
33 {
34 Ok(Some(row)) => match serde_json::from_str::<McpToolSettings>(&row.value) {
35 Ok(settings) => {
36 let _ = cache.set(CACHE_KEY, &settings, Some(CACHE_TTL)).await;
37 settings
38 }
39 Err(error) => {
40 tracing::warn!(%error, "invalid mcp tool settings payload; treating all tools as enabled");
41 McpToolSettings::default()
42 }
43 },
44 Ok(None) => McpToolSettings::default(),
45 Err(error) => {
46 tracing::warn!(%error, "failed to load mcp tool settings; treating all tools as enabled");
47 McpToolSettings::default()
48 }
49 }
50}
51
52pub async fn read_disabled(state: &AppState) -> HashSet<String> {
53 read(state).await.disabled.into_iter().collect()
54}
55
56pub async fn update(
57 state: &AppState,
58 disabled: Vec<String>,
59 updated_by: Option<String>,
60) -> Result<McpToolSettings, String> {
61 let mut disabled: Vec<String> = disabled
62 .into_iter()
63 .map(|name| name.trim().to_string())
64 .filter(|name| !name.is_empty())
65 .collect();
66 disabled.sort();
67 disabled.dedup();
68 let settings = McpToolSettings {
69 disabled,
70 updated_by,
71 updated_at: Some(time::OffsetDateTime::now_utc().to_string()),
72 };
73 let value = serde_json::to_string(&settings).map_err(|error| error.to_string())?;
74 save(state.database.write(), &value)
75 .await
76 .map_err(|error| error.to_string())?;
77 if let Err(error) = export::redis::delete(&state.redis, CACHE_KEY).await {
78 tracing::warn!(%error, "failed to invalidate mcp tool settings cache");
79 }
80 Ok(settings)
81}
82
83async fn save(db: &DatabaseConnection, value: &str) -> Result<(), sea_orm::DbErr> {
84 let now = time::OffsetDateTime::now_utc();
85 let model = system_settings::ActiveModel {
86 key: Set(DB_KEY.to_string()),
87 value: Set(value.to_string()),
88 updated_at: Set(now),
89 };
90 system_settings::Entity::insert(model)
91 .on_conflict(
92 OnConflict::column(system_settings::Column::Key)
93 .update_columns([
94 system_settings::Column::Value,
95 system_settings::Column::UpdatedAt,
96 ])
97 .to_owned(),
98 )
99 .exec_without_returning(db)
100 .await?;
101 Ok(())
102}
Added lib/mcp-api/Cargo.toml View lib/mcp-api/Cargo.toml
@@ -0,0 +1,31 @@
1[package]
2name = "mcp-api"
3version.workspace = true
4edition.workspace = true
5
6[dependencies]
7agent = { workspace = true }
8auth = { workspace = true }
9cache = { workspace = true }
10config = { workspace = true }
11export = { workspace = true }
12git = { workspace = true }
13gitbaby = { workspace = true }
14issues = { workspace = true }
15models = { workspace = true }
16pulls = { workspace = true }
17space = { workspace = true }
18user = { workspace = true }
19axum = { workspace = true }
20base64 = { workspace = true }
21ipnet = { workspace = true }
22rmcp = { workspace = true }
23sea-orm = { workspace = true }
24serde = { workspace = true, features = ["derive"] }
25serde_json = { workspace = true }
26sha2 = { workspace = true }
27time = { workspace = true }
28tokio = { workspace = true }
29tracing = { workspace = true }
30uuid = { workspace = true }
31schemars = { version = "1", features = ["derive", "uuid1"] }