Skip to content

Commit b7a97a0

Browse files
authored
Initial MCP implementation (#81770)
### What? Add MCP for next dev exposing entrypoints, module graph and issues. Can be enabled via `NEXT_EXPERIMENTAL_MCP_SECRET=<random>` and makes the MCP available on `/_next/mcp?<random>`. Interesting change is in: packages/next/src/server/lib/router-utils/mcp.ts
1 parent 94496b8 commit b7a97a0

File tree

23 files changed

+1820
-70
lines changed

23 files changed

+1820
-70
lines changed

crates/napi/src/next_api/endpoint.rs

Lines changed: 112 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,21 +4,25 @@ use anyhow::Result;
44
use futures_util::TryFutureExt;
55
use napi::{JsFunction, bindgen_prelude::External};
66
use next_api::{
7+
module_graph_snapshot::{ModuleGraphSnapshot, get_module_graph_snapshot},
78
operation::OptionEndpoint,
89
paths::ServerPath,
910
route::{
10-
EndpointOutputPaths, endpoint_client_changed_operation, endpoint_server_changed_operation,
11-
endpoint_write_to_disk_operation,
11+
Endpoint, EndpointOutputPaths, endpoint_client_changed_operation,
12+
endpoint_server_changed_operation, endpoint_write_to_disk_operation,
1213
},
1314
};
1415
use tracing::Instrument;
15-
use turbo_tasks::{Completion, Effects, OperationVc, ReadRef, Vc};
16-
use turbopack_core::{diagnostics::PlainDiagnostic, issue::PlainIssue};
16+
use turbo_tasks::{
17+
Completion, Effects, OperationVc, ReadRef, TryFlatJoinIterExt, TryJoinIterExt, Vc,
18+
};
19+
use turbopack_core::{diagnostics::PlainDiagnostic, error::PrettyPrintError, issue::PlainIssue};
1720

1821
use super::utils::{
1922
DetachedVc, NapiDiagnostic, NapiIssue, RootTask, TurbopackResult,
2023
strongly_consistent_catch_collectables, subscribe,
2124
};
25+
use crate::next_api::module_graph::NapiModuleGraphSnapshot;
2226

2327
#[napi(object)]
2428
#[derive(Default)]
@@ -81,6 +85,11 @@ impl From<Option<EndpointOutputPaths>> for NapiWrittenEndpoint {
8185
}
8286
}
8387

88+
#[napi(object)]
89+
pub struct NapiModuleGraphSnapshots {
90+
pub module_graphs: Vec<NapiModuleGraphSnapshot>,
91+
}
92+
8493
// NOTE(alexkirsz) We go through an extra layer of indirection here because of
8594
// two factors:
8695
// 1. rustc currently has a bug where using a dyn trait as a type argument to
@@ -155,6 +164,105 @@ pub async fn endpoint_write_to_disk(
155164
})
156165
}
157166

167+
#[turbo_tasks::value(serialization = "none")]
168+
struct ModuleGraphsWithIssues {
169+
module_graphs: Option<ReadRef<ModuleGraphSnapshots>>,
170+
issues: Arc<Vec<ReadRef<PlainIssue>>>,
171+
diagnostics: Arc<Vec<ReadRef<PlainDiagnostic>>>,
172+
effects: Arc<Effects>,
173+
}
174+
175+
#[turbo_tasks::function(operation)]
176+
async fn get_module_graphs_with_issues_operation(
177+
endpoint_op: OperationVc<OptionEndpoint>,
178+
) -> Result<Vc<ModuleGraphsWithIssues>> {
179+
let module_graphs_op = get_module_graphs_operation(endpoint_op);
180+
let (module_graphs, issues, diagnostics, effects) =
181+
strongly_consistent_catch_collectables(module_graphs_op).await?;
182+
Ok(ModuleGraphsWithIssues {
183+
module_graphs,
184+
issues,
185+
diagnostics,
186+
effects,
187+
}
188+
.cell())
189+
}
190+
191+
#[turbo_tasks::value(transparent)]
192+
struct ModuleGraphSnapshots(Vec<ReadRef<ModuleGraphSnapshot>>);
193+
194+
#[turbo_tasks::function(operation)]
195+
async fn get_module_graphs_operation(
196+
endpoint_op: OperationVc<OptionEndpoint>,
197+
) -> Result<Vc<ModuleGraphSnapshots>> {
198+
let Some(endpoint) = *endpoint_op.connect().await? else {
199+
return Ok(Vc::cell(vec![]));
200+
};
201+
let graphs = endpoint.module_graphs().await?;
202+
let entries = endpoint.entries().await?;
203+
let entry_modules = entries.iter().flat_map(|e| e.entries()).collect::<Vec<_>>();
204+
let snapshots = graphs
205+
.iter()
206+
.map(async |&graph| {
207+
let module_graph = graph.await?;
208+
let entry_modules = entry_modules
209+
.iter()
210+
.map(async |&m| Ok(module_graph.has_entry(m).await?.then_some(m)))
211+
.try_flat_join()
212+
.await?;
213+
Ok((*graph, entry_modules))
214+
})
215+
.try_join()
216+
.await?
217+
.into_iter()
218+
.map(|(graph, entry_modules)| (graph, Vc::cell(entry_modules)))
219+
.collect::<Vec<_>>()
220+
.into_iter()
221+
.map(async |(graph, entry_modules)| {
222+
get_module_graph_snapshot(graph, Some(entry_modules)).await
223+
})
224+
.try_join()
225+
.await?;
226+
Ok(Vc::cell(snapshots))
227+
}
228+
229+
#[napi]
230+
pub async fn endpoint_module_graphs(
231+
#[napi(ts_arg_type = "{ __napiType: \"Endpoint\" }")] endpoint: External<ExternalEndpoint>,
232+
) -> napi::Result<TurbopackResult<NapiModuleGraphSnapshots>> {
233+
let endpoint_op: OperationVc<OptionEndpoint> = ***endpoint;
234+
let (module_graphs, issues, diagnostics) = endpoint
235+
.turbopack_ctx()
236+
.turbo_tasks()
237+
.run_once(async move {
238+
let module_graphs_op = get_module_graphs_with_issues_operation(endpoint_op);
239+
let ModuleGraphsWithIssues {
240+
module_graphs,
241+
issues,
242+
diagnostics,
243+
effects: _,
244+
} = &*module_graphs_op.connect().await?;
245+
Ok((module_graphs.clone(), issues.clone(), diagnostics.clone()))
246+
})
247+
.await
248+
.map_err(|e| napi::Error::from_reason(PrettyPrintError(&e).to_string()))?;
249+
250+
Ok(TurbopackResult {
251+
result: NapiModuleGraphSnapshots {
252+
module_graphs: module_graphs
253+
.into_iter()
254+
.flat_map(|m| m.into_iter())
255+
.map(|m| NapiModuleGraphSnapshot::from(&**m))
256+
.collect(),
257+
},
258+
issues: issues.iter().map(|i| NapiIssue::from(&**i)).collect(),
259+
diagnostics: diagnostics
260+
.iter()
261+
.map(|d| NapiDiagnostic::from(d))
262+
.collect(),
263+
})
264+
}
265+
158266
#[napi(ts_return_type = "{ __napiType: \"RootTask\" }")]
159267
pub fn endpoint_server_changed_subscribe(
160268
#[napi(ts_arg_type = "{ __napiType: \"Endpoint\" }")] endpoint: External<ExternalEndpoint>,

crates/napi/src/next_api/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
pub mod endpoint;
2+
pub mod module_graph;
23
pub mod project;
34
pub mod turbopack_ctx;
45
pub mod utils;
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
use next_api::module_graph_snapshot::{ModuleGraphSnapshot, ModuleInfo, ModuleReference};
2+
use turbo_rcstr::RcStr;
3+
use turbopack_core::chunk::ChunkingType;
4+
5+
#[napi(object)]
6+
pub struct NapiModuleReference {
7+
/// The index of the referenced/referencing module in the modules list.
8+
pub index: u32,
9+
/// The export used in the module reference.
10+
pub export: String,
11+
/// The type of chunking for the module reference.
12+
pub chunking_type: String,
13+
}
14+
15+
impl From<&ModuleReference> for NapiModuleReference {
16+
fn from(reference: &ModuleReference) -> Self {
17+
Self {
18+
index: reference.index as u32,
19+
export: reference.export.to_string(),
20+
chunking_type: match &reference.chunking_type {
21+
ChunkingType::Parallel { hoisted: true, .. } => "hoisted".to_string(),
22+
ChunkingType::Parallel { hoisted: false, .. } => "sync".to_string(),
23+
ChunkingType::Async => "async".to_string(),
24+
ChunkingType::Isolated {
25+
merge_tag: None, ..
26+
} => "isolated".to_string(),
27+
ChunkingType::Isolated {
28+
merge_tag: Some(name),
29+
..
30+
} => format!("isolated {name}"),
31+
ChunkingType::Shared {
32+
merge_tag: None, ..
33+
} => "shared".to_string(),
34+
ChunkingType::Shared {
35+
merge_tag: Some(name),
36+
..
37+
} => format!("shared {name}"),
38+
ChunkingType::Traced => "traced".to_string(),
39+
},
40+
}
41+
}
42+
}
43+
44+
#[napi(object)]
45+
pub struct NapiModuleInfo {
46+
pub ident: RcStr,
47+
pub path: RcStr,
48+
pub depth: u32,
49+
pub size: u32,
50+
pub retained_size: u32,
51+
pub references: Vec<NapiModuleReference>,
52+
pub incoming_references: Vec<NapiModuleReference>,
53+
}
54+
55+
impl From<&ModuleInfo> for NapiModuleInfo {
56+
fn from(info: &ModuleInfo) -> Self {
57+
Self {
58+
ident: info.ident.clone(),
59+
path: info.path.clone(),
60+
depth: info.depth,
61+
size: info.size,
62+
retained_size: info.retained_size,
63+
references: info
64+
.references
65+
.iter()
66+
.map(NapiModuleReference::from)
67+
.collect(),
68+
incoming_references: info
69+
.incoming_references
70+
.iter()
71+
.map(NapiModuleReference::from)
72+
.collect(),
73+
}
74+
}
75+
}
76+
77+
#[napi(object)]
78+
#[derive(Default)]
79+
pub struct NapiModuleGraphSnapshot {
80+
pub modules: Vec<NapiModuleInfo>,
81+
pub entries: Vec<u32>,
82+
}
83+
84+
impl From<&ModuleGraphSnapshot> for NapiModuleGraphSnapshot {
85+
fn from(snapshot: &ModuleGraphSnapshot) -> Self {
86+
Self {
87+
modules: snapshot.modules.iter().map(NapiModuleInfo::from).collect(),
88+
entries: snapshot
89+
.entries
90+
.iter()
91+
.map(|&i| {
92+
// If you have more that 4294967295 entries, you probably have other problems...
93+
i.try_into().unwrap()
94+
})
95+
.collect(),
96+
}
97+
}
98+
}

crates/napi/src/next_api/project.rs

Lines changed: 104 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ use napi::{
99
};
1010
use next_api::{
1111
entrypoints::Entrypoints,
12+
module_graph_snapshot::{ModuleGraphSnapshot, get_module_graph_snapshot},
1213
operation::{
1314
EntrypointsOperation, InstrumentationOperation, MiddlewareOperation, OptionEndpoint,
1415
RouteOperation,
@@ -63,13 +64,14 @@ use url::Url;
6364
use crate::{
6465
next_api::{
6566
endpoint::ExternalEndpoint,
67+
module_graph::NapiModuleGraphSnapshot,
6668
turbopack_ctx::{
6769
NapiNextTurbopackCallbacks, NapiNextTurbopackCallbacksJsObject, NextTurboTasks,
6870
NextTurbopackContext, create_turbo_tasks,
6971
},
7072
utils::{
7173
DetachedVc, NapiDiagnostic, NapiIssue, RootTask, TurbopackResult, get_diagnostics,
72-
get_issues, subscribe,
74+
get_issues, strongly_consistent_catch_collectables, subscribe,
7375
},
7476
},
7577
register,
@@ -987,6 +989,40 @@ async fn output_assets_operation(
987989
Ok(Vc::cell(output_assets.into_iter().collect()))
988990
}
989991

992+
#[napi]
993+
pub async fn project_entrypoints(
994+
#[napi(ts_arg_type = "{ __napiType: \"Project\" }")] project: External<ProjectInstance>,
995+
) -> napi::Result<TurbopackResult<NapiEntrypoints>> {
996+
let container = project.container;
997+
998+
let (entrypoints, issues, diags) = project
999+
.turbopack_ctx
1000+
.turbo_tasks()
1001+
.run_once(async move {
1002+
let entrypoints_with_issues_op = get_entrypoints_with_issues_operation(container);
1003+
1004+
// Read and compile the files
1005+
let EntrypointsWithIssues {
1006+
entrypoints,
1007+
issues,
1008+
diagnostics,
1009+
effects: _,
1010+
} = &*entrypoints_with_issues_op
1011+
.read_strongly_consistent()
1012+
.await?;
1013+
1014+
Ok((entrypoints.clone(), issues.clone(), diagnostics.clone()))
1015+
})
1016+
.await
1017+
.map_err(|e| napi::Error::from_reason(PrettyPrintError(&e).to_string()))?;
1018+
1019+
Ok(TurbopackResult {
1020+
result: NapiEntrypoints::from_entrypoints_op(&entrypoints, &project.turbopack_ctx)?,
1021+
issues: issues.iter().map(|i| NapiIssue::from(&**i)).collect(),
1022+
diagnostics: diags.iter().map(|d| NapiDiagnostic::from(d)).collect(),
1023+
})
1024+
}
1025+
9901026
#[napi(ts_return_type = "{ __napiType: \"RootTask\" }")]
9911027
pub fn project_entrypoints_subscribe(
9921028
#[napi(ts_arg_type = "{ __napiType: \"Project\" }")] project: External<ProjectInstance>,
@@ -1650,3 +1686,70 @@ pub fn project_get_source_map_sync(
16501686
tokio::runtime::Handle::current().block_on(project_get_source_map(project, file_path))
16511687
})
16521688
}
1689+
1690+
#[napi]
1691+
pub async fn project_module_graph(
1692+
#[napi(ts_arg_type = "{ __napiType: \"Project\" }")] project: External<ProjectInstance>,
1693+
) -> napi::Result<TurbopackResult<NapiModuleGraphSnapshot>> {
1694+
let container = project.container;
1695+
let (module_graph, issues, diagnostics) = project
1696+
.turbopack_ctx
1697+
.turbo_tasks()
1698+
.run_once(async move {
1699+
let module_graph_op = get_module_graph_with_issues_operation(container);
1700+
let ModuleGraphWithIssues {
1701+
module_graph,
1702+
issues,
1703+
diagnostics,
1704+
effects: _,
1705+
} = &*module_graph_op.connect().await?;
1706+
Ok((module_graph.clone(), issues.clone(), diagnostics.clone()))
1707+
})
1708+
.await
1709+
.map_err(|e| napi::Error::from_reason(PrettyPrintError(&e).to_string()))?;
1710+
1711+
Ok(TurbopackResult {
1712+
result: module_graph.map_or_else(NapiModuleGraphSnapshot::default, |m| {
1713+
NapiModuleGraphSnapshot::from(&*m)
1714+
}),
1715+
issues: issues.iter().map(|i| NapiIssue::from(&**i)).collect(),
1716+
diagnostics: diagnostics
1717+
.iter()
1718+
.map(|d| NapiDiagnostic::from(d))
1719+
.collect(),
1720+
})
1721+
}
1722+
1723+
#[turbo_tasks::value(serialization = "none")]
1724+
struct ModuleGraphWithIssues {
1725+
module_graph: Option<ReadRef<ModuleGraphSnapshot>>,
1726+
issues: Arc<Vec<ReadRef<PlainIssue>>>,
1727+
diagnostics: Arc<Vec<ReadRef<PlainDiagnostic>>>,
1728+
effects: Arc<Effects>,
1729+
}
1730+
1731+
#[turbo_tasks::function(operation)]
1732+
async fn get_module_graph_with_issues_operation(
1733+
project: ResolvedVc<ProjectContainer>,
1734+
) -> Result<Vc<ModuleGraphWithIssues>> {
1735+
let module_graph_op = get_module_graph_operation(project);
1736+
let (module_graph, issues, diagnostics, effects) =
1737+
strongly_consistent_catch_collectables(module_graph_op).await?;
1738+
Ok(ModuleGraphWithIssues {
1739+
module_graph,
1740+
issues,
1741+
diagnostics,
1742+
effects,
1743+
}
1744+
.cell())
1745+
}
1746+
1747+
#[turbo_tasks::function(operation)]
1748+
async fn get_module_graph_operation(
1749+
project: ResolvedVc<ProjectContainer>,
1750+
) -> Result<Vc<ModuleGraphSnapshot>> {
1751+
let project = project.project();
1752+
let graph = project.whole_app_module_graphs().await?.full;
1753+
let snapshot = get_module_graph_snapshot(*graph, None).resolve().await?;
1754+
Ok(snapshot)
1755+
}

0 commit comments

Comments
 (0)