-
Notifications
You must be signed in to change notification settings - Fork 1k
feat: Integrate MCP Apps into A2UI #748
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
e9b2b20
77e96a2
24cbc89
ab56d17
ad105f4
e22bed7
f5b2f41
22859bf
0f43a44
63a3fd3
04c0782
c1b84a1
62bfcd3
22cfcec
6c53d20
776fa4d
6bf4868
7a544f8
441f282
b80812e
5f3a7db
57091eb
dd1d846
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -274,44 +274,54 @@ def validate(self, a2ui_json: Union[Dict[str, Any], List[Any]]) -> None: | |
| msg += f"\n - {sub_error.message}" | ||
| raise ValueError(msg) | ||
|
|
||
| root_id = _find_root_id(messages) | ||
|
|
||
| for message in messages: | ||
| if not isinstance(message, dict): | ||
| continue | ||
|
|
||
| components = None | ||
| surface_id = None | ||
| if "surfaceUpdate" in message: # v0.8 | ||
| components = message["surfaceUpdate"].get(COMPONENTS) | ||
| surface_id = message["surfaceUpdate"].get("surfaceId") | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We should use constants for "surfaceId" |
||
| elif "updateComponents" in message and isinstance( | ||
| message["updateComponents"], dict | ||
| ): # v0.9 | ||
| components = message["updateComponents"].get(COMPONENTS) | ||
| surface_id = message["updateComponents"].get("surfaceId") | ||
|
|
||
| if components: | ||
| ref_map = _extract_component_ref_fields(self._catalog) | ||
| root_id = _find_root_id(messages, surface_id) | ||
| _validate_component_integrity(root_id, components, ref_map) | ||
| _validate_topology(root_id, components, ref_map) | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Making |
||
|
|
||
| _validate_recursion_and_paths(message) | ||
|
|
||
|
|
||
| def _find_root_id(messages: List[Dict[str, Any]]) -> str: | ||
| def _find_root_id( | ||
| messages: List[Dict[str, Any]], surface_id: Optional[str] = None | ||
| ) -> Optional[str]: | ||
| """ | ||
| Finds the root id from a list of A2UI messages. | ||
| Finds the root id from a list of A2UI messages for a given surface. | ||
| - For v0.8, the root id is in the beginRendering message. | ||
| - For v0.9+, the root id is 'root'. | ||
| """ | ||
| for message in messages: | ||
| if not isinstance(message, dict): | ||
| continue | ||
| if "beginRendering" in message: | ||
| if surface_id and message["beginRendering"].get("surfaceId") != surface_id: | ||
| continue | ||
| return message["beginRendering"].get(ROOT, ROOT) | ||
| return ROOT | ||
| if "createSurface" in message: | ||
| if surface_id and message["createSurface"].get("surfaceId") != surface_id: | ||
| continue | ||
| return ROOT | ||
| return None | ||
|
|
||
|
|
||
| def _validate_component_integrity( | ||
| root_id: str, | ||
| root_id: Optional[str], | ||
| components: List[Dict[str, Any]], | ||
| ref_fields_map: Dict[str, tuple[Set[str], Set[str]]], | ||
| ) -> None: | ||
|
|
@@ -334,21 +344,23 @@ def _validate_component_integrity( | |
| ids.add(comp_id) | ||
|
|
||
| # 2. Check for root component | ||
| if root_id not in ids: | ||
| if root_id is not None and root_id not in ids: | ||
| raise ValueError(f"Missing root component: No component has id='{root_id}'") | ||
|
|
||
| # 3. Check for dangling references using helper | ||
| for comp in components: | ||
| for ref_id, field_name in _get_component_references(comp, ref_fields_map): | ||
| if ref_id not in ids: | ||
| raise ValueError( | ||
| f"Component '{comp.get(ID)}' references non-existent component '{ref_id}'" | ||
| f" in field '{field_name}'" | ||
| ) | ||
| # In an incremental update (root_id is None), components may reference IDs already on the client. | ||
| if root_id is not None: | ||
| for comp in components: | ||
| for ref_id, field_name in _get_component_references(comp, ref_fields_map): | ||
| if ref_id not in ids: | ||
| raise ValueError( | ||
| f"Component '{comp.get(ID)}' references non-existent component '{ref_id}'" | ||
| f" in field '{field_name}'" | ||
| ) | ||
|
|
||
|
|
||
| def _validate_topology( | ||
| root_id: str, | ||
| root_id: Optional[str], | ||
| components: List[Dict[str, Any]], | ||
| ref_fields_map: Dict[str, tuple[Set[str], Set[str]]], | ||
| ) -> None: | ||
|
|
@@ -401,16 +413,22 @@ def dfs(node_id: str, depth: int): | |
|
|
||
| recursion_stack.remove(node_id) | ||
|
|
||
| if root_id in all_ids: | ||
| dfs(root_id, 0) | ||
| if root_id is not None: | ||
| if root_id in all_ids: | ||
| dfs(root_id, 0) | ||
|
|
||
| # Check for Orphans | ||
| orphans = all_ids - visited | ||
| if orphans: | ||
| sorted_orphans = sorted(list(orphans)) | ||
| raise ValueError( | ||
| f"Component '{sorted_orphans[0]}' is not reachable from '{root_id}'" | ||
| ) | ||
| # Check for Orphans | ||
| orphans = all_ids - visited | ||
| if orphans: | ||
| sorted_orphans = sorted(list(orphans)) | ||
| raise ValueError( | ||
| f"Component '{sorted_orphans[0]}' is not reachable from '{root_id}'" | ||
| ) | ||
| else: | ||
| # Partial update: we cannot check root reachability, but we still check for cycles | ||
| for node_id in all_ids: | ||
| if node_id not in visited: | ||
| dfs(node_id, 0) | ||
|
|
||
|
|
||
| def _extract_component_ref_fields( | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a bit of a code smell to me.
In line 139 we add a prescription that client can only provide one or the other; inline or supported catalog.
But here, we are now implicitly baking the standard catalog.
1 ) Clients might NOT want standard catalog components included in responses.
2) component name overlaps could result in unexpected behavior without safe-guards
Can we revert this change and perhaps consider a more hi-fi solution to achieve this effect?