agentshield/adapter/
mcp.rs1use std::path::Path;
2
3use crate::error::Result;
4use crate::ir::execution_surface::ExecutionSurface;
5use crate::ir::*;
6use crate::parser;
7
8pub struct McpAdapter;
15
16impl super::Adapter for McpAdapter {
17 fn framework(&self) -> Framework {
18 Framework::Mcp
19 }
20
21 fn detect(&self, root: &Path) -> bool {
22 let pkg_json = root.join("package.json");
24 if pkg_json.exists() {
25 if let Ok(content) = std::fs::read_to_string(&pkg_json) {
26 if content.contains("@modelcontextprotocol/sdk") || content.contains("mcp-server") {
27 return true;
28 }
29 }
30 }
31
32 let pyproject = root.join("pyproject.toml");
34 if pyproject.exists() {
35 if let Ok(content) = std::fs::read_to_string(&pyproject) {
36 if content.contains("mcp") {
37 return true;
38 }
39 }
40 }
41
42 if let Ok(entries) = std::fs::read_dir(root) {
44 for entry in entries.flatten() {
45 let path = entry.path();
46 if path.extension().is_some_and(|e| e == "py") {
47 if let Ok(content) = std::fs::read_to_string(&path) {
48 if content.contains("from mcp")
49 || content.contains("import mcp")
50 || content.contains("@server.tool")
51 {
52 return true;
53 }
54 }
55 }
56 }
57 }
58
59 let requirements = root.join("requirements.txt");
61 if requirements.exists() {
62 if let Ok(content) = std::fs::read_to_string(&requirements) {
63 if content.lines().any(|l| l.trim().starts_with("mcp")) {
64 return true;
65 }
66 }
67 }
68
69 false
70 }
71
72 fn load(&self, root: &Path) -> Result<Vec<ScanTarget>> {
73 let name = root
74 .file_name()
75 .map(|n| n.to_string_lossy().to_string())
76 .unwrap_or_else(|| "mcp-server".into());
77
78 let mut source_files = Vec::new();
79 let mut execution = ExecutionSurface::default();
80 let mut tools = Vec::new();
81
82 collect_source_files(root, &mut source_files)?;
84
85 for sf in &source_files {
87 if let Some(parser) = parser::parser_for_language(sf.language) {
88 if let Ok(parsed) = parser.parse_file(&sf.path, &sf.content) {
89 execution.commands.extend(parsed.commands);
90 execution.file_operations.extend(parsed.file_operations);
91 execution
92 .network_operations
93 .extend(parsed.network_operations);
94 execution.env_accesses.extend(parsed.env_accesses);
95 execution.dynamic_exec.extend(parsed.dynamic_exec);
96 }
97 }
98 }
99
100 let tools_json = root.join("tools.json");
102 if tools_json.exists() {
103 if let Ok(content) = std::fs::read_to_string(&tools_json) {
104 if let Ok(value) = serde_json::from_str::<serde_json::Value>(&content) {
105 tools = parser::json_schema::parse_tools_from_json(&value);
106 }
107 }
108 }
109
110 let dependencies = parse_dependencies(root);
112
113 let provenance = parse_provenance(root);
115
116 Ok(vec![ScanTarget {
117 name,
118 framework: Framework::Mcp,
119 root_path: root.to_path_buf(),
120 tools,
121 execution,
122 data: Default::default(),
123 dependencies,
124 provenance,
125 source_files,
126 }])
127 }
128}
129
130fn collect_source_files(root: &Path, files: &mut Vec<SourceFile>) -> Result<()> {
131 let walker = ignore::WalkBuilder::new(root)
132 .hidden(true)
133 .git_ignore(true)
134 .max_depth(Some(5))
135 .build();
136
137 for entry in walker.flatten() {
138 let path = entry.path();
139 if !path.is_file() {
140 continue;
141 }
142
143 let ext = path
144 .extension()
145 .map(|e| e.to_string_lossy().to_string())
146 .unwrap_or_default();
147 let lang = Language::from_extension(&ext);
148
149 if matches!(lang, Language::Unknown) {
150 continue;
151 }
152
153 let metadata = std::fs::metadata(path)?;
155 if metadata.len() > 1_048_576 {
156 continue;
157 }
158
159 if let Ok(content) = std::fs::read_to_string(path) {
160 let hash = format!(
161 "{:x}",
162 sha2::Digest::finalize(sha2::Sha256::new().chain_update(content.as_bytes()))
163 );
164 files.push(SourceFile {
165 path: path.to_path_buf(),
166 language: lang,
167 size_bytes: metadata.len(),
168 content_hash: hash,
169 content,
170 });
171 }
172 }
173
174 Ok(())
175}
176
177fn parse_dependencies(root: &Path) -> dependency_surface::DependencySurface {
178 use crate::ir::dependency_surface::*;
179 let mut surface = DependencySurface::default();
180
181 let req_file = root.join("requirements.txt");
183 if req_file.exists() {
184 if let Ok(content) = std::fs::read_to_string(&req_file) {
185 for (idx, line) in content.lines().enumerate() {
186 let line = line.trim();
187 if line.is_empty() || line.starts_with('#') || line.starts_with('-') {
188 continue;
189 }
190 let (name, version) = if let Some(pos) = line.find("==") {
191 (
192 line[..pos].trim().to_string(),
193 Some(line[pos + 2..].trim().to_string()),
194 )
195 } else if let Some(pos) = line.find(">=") {
196 (
197 line[..pos].trim().to_string(),
198 Some(line[pos..].trim().to_string()),
199 )
200 } else {
201 (line.to_string(), None)
202 };
203
204 surface.dependencies.push(Dependency {
205 name,
206 version_constraint: version,
207 locked_version: None,
208 locked_hash: None,
209 registry: "pypi".into(),
210 is_dev: false,
211 location: Some(SourceLocation {
212 file: req_file.clone(),
213 line: idx + 1,
214 column: 0,
215 end_line: None,
216 end_column: None,
217 }),
218 });
219 }
220 }
221 }
222
223 for (filename, format) in [
225 ("Pipfile.lock", LockfileFormat::PipenvLock),
226 ("poetry.lock", LockfileFormat::PoetryLock),
227 ("uv.lock", LockfileFormat::UvLock),
228 ] {
229 let lock_path = root.join(filename);
230 if lock_path.exists() {
231 surface.lockfile = Some(LockfileInfo {
232 path: lock_path,
233 format,
234 all_pinned: true,
235 all_hashed: false,
236 });
237 break;
238 }
239 }
240
241 let pkg_json = root.join("package.json");
243 if pkg_json.exists() {
244 if let Ok(content) = std::fs::read_to_string(&pkg_json) {
245 if let Ok(value) = serde_json::from_str::<serde_json::Value>(&content) {
246 for (key, is_dev) in [("dependencies", false), ("devDependencies", true)] {
247 if let Some(deps) = value.get(key).and_then(|v| v.as_object()) {
248 for (name, version) in deps {
249 surface.dependencies.push(Dependency {
250 name: name.clone(),
251 version_constraint: version.as_str().map(|s| s.to_string()),
252 locked_version: None,
253 locked_hash: None,
254 registry: "npm".into(),
255 is_dev,
256 location: None,
257 });
258 }
259 }
260 }
261 }
262 }
263
264 let lock = root.join("package-lock.json");
266 if lock.exists() {
267 surface.lockfile = Some(LockfileInfo {
268 path: lock,
269 format: dependency_surface::LockfileFormat::NpmLock,
270 all_pinned: true,
271 all_hashed: false,
272 });
273 }
274 }
275
276 surface
277}
278
279fn parse_provenance(root: &Path) -> provenance_surface::ProvenanceSurface {
280 let mut prov = provenance_surface::ProvenanceSurface::default();
281
282 let pkg_json = root.join("package.json");
284 if pkg_json.exists() {
285 if let Ok(content) = std::fs::read_to_string(&pkg_json) {
286 if let Ok(value) = serde_json::from_str::<serde_json::Value>(&content) {
287 prov.author = value
288 .get("author")
289 .and_then(|v| v.as_str())
290 .map(|s| s.to_string());
291 prov.repository = value
292 .get("repository")
293 .and_then(|v| v.get("url").or(Some(v)))
294 .and_then(|v| v.as_str())
295 .map(|s| s.to_string());
296 prov.license = value
297 .get("license")
298 .and_then(|v| v.as_str())
299 .map(|s| s.to_string());
300 }
301 }
302 }
303
304 let pyproject = root.join("pyproject.toml");
306 if pyproject.exists() {
307 if let Ok(content) = std::fs::read_to_string(&pyproject) {
308 if let Ok(value) = content.parse::<toml::Value>() {
309 if let Some(project) = value.get("project") {
310 prov.license = project
311 .get("license")
312 .and_then(|v| v.get("text").or(Some(v)))
313 .and_then(|v| v.as_str())
314 .map(|s| s.to_string());
315 if let Some(authors) = project.get("authors").and_then(|v| v.as_array()) {
316 if let Some(first) = authors.first() {
317 prov.author = first
318 .get("name")
319 .and_then(|v| v.as_str())
320 .map(|s| s.to_string());
321 }
322 }
323 }
324 if let Some(urls) = value.get("project").and_then(|p| p.get("urls")) {
325 prov.repository = urls
326 .get("Repository")
327 .or(urls.get("repository"))
328 .and_then(|v| v.as_str())
329 .map(|s| s.to_string());
330 }
331 }
332 }
333 }
334
335 prov
336}
337
338use sha2::Digest;