Skip to content

Comments

feat: OpenHexa MCP server (HEXA-1507)#1638

Open
yolanfery wants to merge 21 commits intomainfrom
POC3
Open

feat: OpenHexa MCP server (HEXA-1507)#1638
yolanfery wants to merge 21 commits intomainfrom
POC3

Conversation

@yolanfery
Copy link
Contributor

@yolanfery yolanfery commented Feb 23, 2026

Deploy a remote MCP server with basic tools secured via OAuth 2.1

Note : some areas are not up to our standard quality but this PR serves as a POC to get early feedback and to understand where we should focus first

Changes

  • MCP server relaying queries using RPC Json
  • OAuth 2.1 to allow Claude, ChatGPT to authorize usage

How/what to test

  • Start NGROK to expose your app to the world
  • You get an URL like https://tommy-someurl-terese.ngrok-free.dev
  • Use your url + /mcp to configure it in Claude (https://tommy-someurl-terese.ngrok-free.dev/mcp)
  • Go through the authorization steps
  • Chat with Claude

Screenshots / screencast

Screenshot 2026-02-23 at 19 51 26 Screenshot 2026-02-23 at 19 51 53 Screenshot 2026-02-23 at 19 52 29 Screenshot 2026-02-23 at 19 52 50

To go further

  • Audit action per user and show them in the UI or admin panel
  • Granular authorization (accept only some workspaces/actions)
  • Tool to get example of pipelines (like validated templates)

if result is None:
return HttpResponse(status=204)

response = JsonResponse(result, status=200)

Check warning

Code scanning / CodeQL

Information exposure through an exception Medium

Stack trace information flows to this location and may be exposed to an external user.

Copilot Autofix

AI about 16 hours ago

In general, to fix information exposure through exceptions, keep detailed error information (messages, stack traces) in server-side logs and return only generic, high-level error messages to clients. This preserves debuggability for developers without revealing internal details to callers.

For this specific code, the risky behavior is in backend/hexa/mcp/protocol.py in the handle_jsonrpc function, inside the if method == "tools/call": block. On exception, we currently build a JSON-RPC result that includes "text": str(e). We should instead:

  • Keep the logger.exception("Tool call failed: %s", tool_name) call so the full traceback is logged.
  • Replace the client-facing "text": str(e) with a generic error string that does not include details from the exception, e.g. "Tool call failed" or "An internal error occurred while running the tool.".
  • Optionally, include the tool name or a generic hint if that is acceptable from a privacy standpoint, but avoid echoing exception contents or stack traces.

No changes are needed in backend/hexa/mcp/views.py; the mcp_endpoint view can continue to return the JSON returned by handle_jsonrpc. Only handle_jsonrpc’s error payload needs to be adjusted.

Concretely:

  • Edit backend/hexa/mcp/protocol.py around lines 106–115.
  • In the returned dict under "content", replace str(e) with a hard-coded, non-sensitive message.
  • Do not modify imports or logging configuration.
Suggested changeset 1
backend/hexa/mcp/protocol.py
Outside changed files

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/backend/hexa/mcp/protocol.py b/backend/hexa/mcp/protocol.py
--- a/backend/hexa/mcp/protocol.py
+++ b/backend/hexa/mcp/protocol.py
@@ -109,7 +109,12 @@
                 "jsonrpc": "2.0",
                 "id": req_id,
                 "result": {
-                    "content": [{"type": "text", "text": str(e)}],
+                    "content": [
+                        {
+                            "type": "text",
+                            "text": "An internal error occurred while running the tool.",
+                        }
+                    ],
                     "isError": True,
                 },
             }
EOF
@@ -109,7 +109,12 @@
"jsonrpc": "2.0",
"id": req_id,
"result": {
"content": [{"type": "text", "text": str(e)}],
"content": [
{
"type": "text",
"text": "An internal error occurred while running the tool.",
}
],
"isError": True,
},
}
Copilot is powered by AI and may make mistakes. Always verify output.
Unable to commit as this autofix suggestion is now outdated
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

Comment on lines +553 to +554
"ACCESS_TOKEN_EXPIRE_SECONDS": 3600,
"REFRESH_TOKEN_EXPIRE_SECONDS": 86400,
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have not tested expiration, something to consider, understand how tools are handling those expirations

@@ -0,0 +1,82 @@
query ListWorkspaces($query: String, $page: Int, $perPage: Int) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lets use our graphql layer to avoid code repetition

token=token
)
if access_token.expires >= timezone.now():
request.user = access_token.user
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The magic to scope actions to user

Comment on lines +14 to +16
def tool(func):
_TOOLS[func.__name__] = func
return func
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No framework/library, just a small registry of tools

return func(**arguments)


def handle_jsonrpc(body: bytes, user) -> dict | None:
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Obviously some parts of this protocol is yet unclear to me (error numbers,...)

But I think we can live with unclear parts of a protocol as long as we verify security

request.user = user
request.bypass_two_factor = True

result = graphql_sync(
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Magic to query our graphql layer directly

Comment on lines +104 to +105
# TODO : those tools should be GraphQL mutations instead of REST endpoints, to be consistent with the rest of the API and to leverage GraphQL permissions, validation and audit (to be added).
# For now we keep them as simple tools for internal use, but we should migrate them to GraphQL in the future.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can also ignore them for a v1. But this part of the impressive capabilities of the POC, writing files

In any case they need to be re-worked, don't want to merge those like this.

):
return redirect(f"{reverse(settings.LOGIN_URL)}?next={request.path}")
return redirect(
f"{reverse(settings.LOGIN_URL)}?next={quote(request.get_full_path())}"
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ensure the connection URL used by LLM tools is fully passed after the login blocker

@@ -0,0 +1,201 @@
import json
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lot of lack of understanding here but this is mostly a protocol with some configs

@yolanfery yolanfery marked this pull request as ready for review February 24, 2026 07:59
@yolanfery yolanfery requested a review from bramj February 24, 2026 07:59
@mrivar mrivar self-requested a review February 24, 2026 10:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant