-
Notifications
You must be signed in to change notification settings - Fork 105
Feature/commands #12
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
base: master
Are you sure you want to change the base?
Feature/commands #12
Changes from all commits
86729e5
b20c79d
85e3ad6
6235a3f
055c2f7
43cd36e
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 |
|---|---|---|
|
|
@@ -15,7 +15,11 @@ | |
|
|
||
|
|
||
| def register_code_execution_routes(api): | ||
| """Register code execution routes with the API.""" | ||
| """Register code execution routes with the API. | ||
|
|
||
| ** WARNING ** : this can run 'ANY' code you give it, this can be a major security risk, only send it code that you trust. | ||
|
|
||
| """ | ||
|
|
||
| @api.route("/execute_code/", methods=["POST"]) | ||
| def execute_code(doc, uidoc, request): | ||
|
|
@@ -26,10 +30,10 @@ def execute_code(doc, uidoc, request): | |
| { | ||
| "code": "python code as string", | ||
| "description": "optional description of what the code does", | ||
| "use_transaction": true # set false for UI ops like switching the active view | ||
| } | ||
| """ | ||
| try: | ||
|
|
||
| # Parse the request data | ||
| data = ( | ||
| json.loads(request.data) | ||
|
|
@@ -84,7 +88,6 @@ def execute_code(doc, uidoc, request): | |
| except Exception as exec_error: | ||
| sys.stdout = old_stdout | ||
| partial_output = captured_output.getvalue() | ||
| captured_output.close() | ||
|
|
||
| error_traceback = traceback.format_exc() | ||
| error_type = type(exec_error).__name__ | ||
|
|
@@ -136,5 +139,8 @@ def execute_code(doc, uidoc, request): | |
| except Exception as e: | ||
| logger.error("Execute code request failed: {}".format(str(e))) | ||
| return routes.make_response(data={"error": str(e)}, status=500) | ||
| finally: | ||
| sys.stdout = old_stdout | ||
| captured_output.close() | ||
|
|
||
|
Comment on lines
139
to
145
|
||
| logger.info("Code execution routes registered successfully.") | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,163 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # -*- coding: UTF-8 -*- | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Commands Module for Revit MCP | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Handles Command execution | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from pyrevit import HOST_APP | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from pyrevit.coreutils.logger import get_logger | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from pyrevit.loader import sessioninfo | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import glob | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import os | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from pyrevit import routes | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from pyrevit.routes.server import serverinfo | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from pyrevit import routes | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+6
to
+16
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from pyrevit import HOST_APP | |
| from pyrevit.coreutils.logger import get_logger | |
| from pyrevit.loader import sessioninfo | |
| import glob | |
| import os | |
| from pyrevit import routes | |
| from pyrevit.routes.server import serverinfo | |
| from pyrevit import routes | |
| import glob | |
| import os |
Copilot
AI
Mar 3, 2026
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.
get_commands returns a raw Python list instead of using routes.make_response(...). Other route modules consistently wrap responses with routes.make_response for a stable JSON shape and status handling (e.g. revit_mcp/status.py, revit_mcp/placement.py). Please wrap this list in routes.make_response(data={...}) (or at least data=commands).
Copilot
AI
Mar 3, 2026
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.
Spelling in the docstring: immediatly should be immediately. Also Wait = false is inconsistently capitalized compared to wait elsewhere in the same docstring.
| wait = true => runs execution immediatly and returns script logs (where possible) | |
| Wait = false => posts command to revit ui thread, to be executed after this api call. | |
| wait = true => runs execution immediately and returns script logs (where possible) | |
| wait = false => posts command to revit ui thread, to be executed after this api call. |
Copilot
AI
Mar 3, 2026
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.
data = request.data or {} assumes request.data is already a dict. In other POST routes, request.data is often a JSON string and is parsed with json.loads(...). As-is, calling data.get(...) will fail when request.data is a string; parse the JSON like the other endpoints before reading control_id/wait.
Copilot
AI
Mar 3, 2026
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.
UI.RevitCommandId.LookupCommandId(...) is used but UI is never imported in this module, which will raise NameError on the wait=false path. Import the required Revit UI namespace (or access it via an existing project pattern) before using UI.RevitCommandId.
Copilot
AI
Mar 3, 2026
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.
run_command returns bare dicts for both success and error cases, while other endpoints use routes.make_response(...) to control HTTP status codes and keep a consistent response envelope. Consider returning routes.make_response(data=..., status=...) here as well (e.g. 400 for missing control_id, 404 for unknown commands).
| return {"error": "control_id is required in request body"} | |
| # fire-and-forget via PostCommand | |
| if not wait: | |
| command_id = UI.RevitCommandId.LookupCommandId(control_id) | |
| if command_id is None: | |
| return {"error": "Command not found: {}".format(control_id)} | |
| uiapp.PostCommand(command_id) | |
| return {"result": "posted", "control_id": control_id} | |
| cmd = next((c for c in sessionmgr.find_all_commands() | |
| if c.control_id == control_id), None) | |
| if cmd is None: | |
| return {"error": "Command not found: {}".format(control_id)} | |
| # PyRevit reload destroys the IronPython engine mid-execution, so the HTTP | |
| # response can never be sent. Waiting on it will always time out so we don't allow this | |
| # command to be run with 'wait' | |
| if cmd.unique_id == 'pyrevitcore_pyrevit_pyrevit_tools_reload': | |
| return { | |
| "error": "Cannot await PyRevit reload: the reload script destroys " | |
| "the runtime engine before a response can be sent. " | |
| "Use wait=false to fire-and-forget instead." | |
| } | |
| return routes.make_response( | |
| data={"error": "control_id is required in request body"}, | |
| status=400, | |
| ) | |
| # fire-and-forget via PostCommand | |
| if not wait: | |
| command_id = UI.RevitCommandId.LookupCommandId(control_id) | |
| if command_id is None: | |
| return routes.make_response( | |
| data={"error": "Command not found: {}".format(control_id)}, | |
| status=404, | |
| ) | |
| uiapp.PostCommand(command_id) | |
| return routes.make_response( | |
| data={"result": "posted", "control_id": control_id}, | |
| status=200, | |
| ) | |
| cmd = next((c for c in sessionmgr.find_all_commands() | |
| if c.control_id == control_id), None) | |
| if cmd is None: | |
| return routes.make_response( | |
| data={"error": "Command not found: {}".format(control_id)}, | |
| status=404, | |
| ) | |
| # PyRevit reload destroys the IronPython engine mid-execution, so the HTTP | |
| # response can never be sent. Waiting on it will always time out so we don't allow this | |
| # command to be run with 'wait' | |
| if cmd.unique_id == 'pyrevitcore_pyrevit_pyrevit_tools_reload': | |
| return routes.make_response( | |
| data={ | |
| "error": "Cannot await PyRevit reload: the reload script destroys " | |
| "the runtime engine before a response can be sent. " | |
| "Use wait=false to fire-and-forget instead." | |
| }, | |
| status=400, | |
| ) |
Copilot
AI
Mar 3, 2026
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.
In the exception handler, except_info = except_info is a no-op and drops the real exception details. Assign except_info from the caught exception (and ideally include traceback) so the response can report what failed.
Copilot
AI
Mar 3, 2026
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.
envvars and mlogger are referenced but never defined/imported, so the wait=true execution path will raise NameError. Either import/initialize these (e.g. set mlogger = get_logger(__name__) and import envvars from pyRevit) or use the already-defined logger.
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,37 @@ | ||||||||||||||||||||||||||||||||||||||||||||
| # -*- coding: utf-8 -*- | ||||||||||||||||||||||||||||||||||||||||||||
| """Commands tools for the MCP server.""" | ||||||||||||||||||||||||||||||||||||||||||||
| from mcp.server.fastmcp import Context | ||||||||||||||||||||||||||||||||||||||||||||
| from .utils import format_response | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| def register_commands_tools(mcp, revit_get, revit_post, revit_image=None): | ||||||||||||||||||||||||||||||||||||||||||||
| """Register your tools with the MCP server.""" | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| # ---- Tool for the GET request ---- | ||||||||||||||||||||||||||||||||||||||||||||
| @mcp.tool() | ||||||||||||||||||||||||||||||||||||||||||||
| async def list_commands( | ||||||||||||||||||||||||||||||||||||||||||||
| ctx: Context = None | ||||||||||||||||||||||||||||||||||||||||||||
| ) -> str: | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||||||||||||||
| Return a list of all pyrevit commands, including control & uniqueid | ||||||||||||||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||||||||||||||
| response = await revit_get("/commands_run", ctx) | ||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||
| response = await revit_get("/commands_run", ctx) | |
| response = await revit_get("/commands_list", ctx) |
Copilot
AI
Mar 3, 2026
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.
run_command_by_control_id has a non-default parameter (ctx) after default parameters, which is a Python syntax error. Make ctx optional with a default (like other tools) and/or reorder parameters so all non-defaults come first.
Copilot
AI
Mar 3, 2026
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.
The tool sends a config flag in the payload, but /commands_run currently ignores config entirely (it only reads control_id and wait). Either implement config handling server-side or remove it from the tool payload to avoid a misleading API/tool contract.
| async def run_command_by_control_id(control_id: str, config:bool = False, wait:bool=False ,ctx: Context) -> str: | |
| """ | |
| Runs a command using its control_id | |
| Args: | |
| control_id: The ID of the command, as found in the revit journal or list_commands tool | |
| config: Run tool in config (shift+click) mode. | |
| wait: Wait for tool to finish and return response | |
| """ | |
| payload = {"control_id": control_id, "config": config, "wait" = wait} | |
| async def run_command_by_control_id(control_id: str, wait: bool = False, ctx: Context = None) -> str: | |
| """ | |
| Runs a command using its control_id | |
| Args: | |
| control_id: The ID of the command, as found in the revit journal or list_commands tool | |
| wait: Wait for tool to finish and return response | |
| """ | |
| payload = {"control_id": control_id, "wait": wait} |
Copilot
AI
Mar 3, 2026
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.
The payload dict uses "wait" = wait, which is invalid Python syntax (assignment inside a dict literal). Use a : for the key/value pair, and ensure the payload keys match what /commands_run actually supports.
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.
I removed this because it didn't seem to be used