Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion application/single_app/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@
EXECUTOR_TYPE = 'thread'
EXECUTOR_MAX_WORKERS = 30
SESSION_TYPE = 'filesystem'
VERSION = "0.261.030"
VERSION = "0.261.031"
IS_DEVELOPMENT = is_development_env_enabled()

# Opt-out for deployments where App Service Easy Auth is active but the platform
Expand Down
18 changes: 14 additions & 4 deletions application/single_app/functions_action_connection_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -526,16 +526,26 @@
return result


def test_mcp_connection(manifest: Dict[str, Any]) -> Dict[str, Any]:
def test_mcp_connection(manifest: Dict[str, Any], *, origin=None) -> Dict[str, Any]:
"""Validate an MCP action by initializing a session and listing the server's tools."""
# MCP runtime dependencies are needed only when this connection tester is selected.
from functions_mcp_operations import (
McpRuntimeError,
classify_mcp_exception,
get_mcp_error_http_status,
)
from semantic_kernel_plugins.mcp_plugin_factory import McpPluginFactory

try:
probe_result = asyncio.run(McpPluginFactory.probe_server_from_config(manifest))
probe_result = asyncio.run(McpPluginFactory.probe_server_from_config(manifest, origin=origin))

Check warning on line 540 in application/single_app/functions_action_connection_tests.py

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains AI, plugin, agent, or workspace boundary marker. Recommendation%3A Check whether prompts, chat history, uploaded documents, embeddings, citations, settings, or identity can cross a new boundary.
except Exception as exc:
error_info = classify_mcp_exception(exc, "capability_probe")
category = exc.category if isinstance(exc, McpRuntimeError) else error_info["category"]
result = build_failure_result(
f"The MCP server connection failed: {sanitize_connection_error(exc, manifest)}",
status=502,
error_info["message"],
status=get_mcp_error_http_status(category),
error_type=category,
retryable=False,
)
_log_connection_test("mcp", result)
return result
Expand Down
159 changes: 159 additions & 0 deletions application/single_app/functions_action_manifest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
# functions_action_manifest.py
"""Dependency-free action type, origin, and retired-transport contracts."""

from copy import deepcopy
from dataclasses import dataclass
import re


MCP_STDIO_REMOVED_CODE = "mcp_stdio_removed"
MCP_STDIO_REMOVED_MESSAGE = (
"This action uses stdio, which is no longer supported. "
"Reconfigure it to use a supported remote MCP server, or delete it."
)
MCP_TYPE_ALIASES = frozenset({
"mcp", "mcpplugin", "modelcontextprotocol", "modelcontextprotocolplugin",

Check warning on line 15 in application/single_app/functions_action_manifest.py

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains AI, plugin, agent, or workspace boundary marker. Recommendation%3A Check whether prompts, chat history, uploaded documents, embeddings, citations, settings, or identity can cross a new boundary.
})


class McpConfigurationError(ValueError):
"""An invalid MCP configuration with a stable, safe public message."""

def __init__(self, message="MCP configuration is invalid.", code="validation"):
super().__init__(message)
self.public_message = message
self.code = code


class McpStdioRemovedError(McpConfigurationError):
"""A retired configuration must never reach an executable connector."""

def __init__(self):
super().__init__(MCP_STDIO_REMOVED_MESSAGE, MCP_STDIO_REMOVED_CODE)


def resolve_action_type(manifest):
"""Resolve supported MCP aliases without collapsing other runtime types."""
if not isinstance(manifest, dict):
raise ValueError("Action configuration must be an object.")
declared_type = manifest.get("type")
if declared_type is None or (isinstance(declared_type, str) and not declared_type.strip()):
metadata = manifest.get("metadata")
declared_type = metadata.get("type", "") if isinstance(metadata, dict) else ""
if not isinstance(declared_type, str):
raise ValueError("Action type must be a string.")
declared_type = declared_type.strip()
compact_type = re.sub(r"[\s_-]", "", declared_type).lower()
return "mcp" if compact_type in MCP_TYPE_ALIASES else declared_type


def is_mcp_action(manifest):
"""Return whether the effective runtime action is MCP."""
return resolve_action_type(manifest) == "mcp"


def is_retired_mcp_stdio(manifest):
"""Inspect historical records without running active-config normalization."""
if not isinstance(manifest, dict):
return False
try:

Check warning on line 59 in application/single_app/functions_action_manifest.py

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains security control, sanitization, or audit marker. Recommendation%3A Confirm the change does not weaken auth, CSRF, CSP, XSS defenses, settings sanitization, redaction, audit logging, or tests.
if not is_mcp_action(manifest):
return False
except ValueError:
return False
fields = manifest.get("additionalFields")
transport = fields.get("transport", "") if isinstance(fields, dict) else ""
return (
str(transport or "").strip().lower() == "stdio"
or str(manifest.get("endpoint") or "").strip().lower().startswith("stdio:")
)


def get_action_execution_status(manifest):
"""Derive management information; never use it as execution authority."""
if is_retired_mcp_stdio(manifest):
return {
"state": "unsupported",
"code": MCP_STDIO_REMOVED_CODE,
"message": MCP_STDIO_REMOVED_MESSAGE,
}
return None


@dataclass(frozen=True)
class McpActionOrigin:
"""Authoritative origin established by a server-side authorized lookup."""

scope_type: str
scope_id: str
action_id: str = ""

def __post_init__(self):
if self.scope_type not in {"personal", "group", "global"}:
raise ValueError("Action origin has an unsupported scope.")
if not isinstance(self.scope_id, str) or not self.scope_id.strip():
raise ValueError("Action origin requires a scope identifier.")
if self.scope_type == "global" and self.scope_id != "global":
raise ValueError("Global action origin requires the global scope.")
if not isinstance(self.action_id, str):
raise ValueError("Action origin has an invalid action identifier.")


class ScopedActionManifest(dict):
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
"""Compare JSON payloads, keeping server-only authorization origin separate."""

Check warning on line 103 in application/single_app/functions_action_manifest.py

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains secret or sensitive data source marker. Recommendation%3A Pair this source with any nearby network, logging, serialization, or process execution sink before approving.

def __init__(self, manifest, origin):
if not isinstance(origin, McpActionOrigin):
raise ValueError("Action origin is required.")
super().__init__(manifest)
self._action_origin = origin

def __eq__(self, other):
return dict.__eq__(self, other)

def __ne__(self, other):
return dict.__ne__(self, other)

@property
def action_origin(self):
return self._action_origin

def copy(self):
return ScopedActionManifest(self, self._action_origin)


def get_action_origin(manifest):
"""Never interpret dictionary keys as proof of a trusted origin."""
return manifest.action_origin if isinstance(manifest, ScopedActionManifest) else None


def copy_action_manifest(manifest):
"""Copy an internal manifest without discarding its lookup provenance."""
return deepcopy(manifest)


def bind_action_origin(manifest, scope_type, scope_id):
"""Bind a record to the collection/partition used by its authorized caller."""
origin = McpActionOrigin(
scope_type=scope_type,
scope_id=str(scope_id or ""),
action_id=str(manifest.get("id") or ""),
)
bound = ScopedActionManifest(deepcopy(dict(manifest)), origin)
bound["type"] = resolve_action_type(bound)
for field in ("runtime_user_id", "action_origin", "_action_origin", "execution_status"):
bound.pop(field, None)
bound["scope"] = "user" if scope_type == "personal" else scope_type
bound["scope_id"] = origin.scope_id
bound["is_global"] = scope_type == "global"
bound["is_group"] = scope_type == "group"
if scope_type == "personal":
bound["user_id"] = origin.scope_id
bound.pop("group_id", None)
elif scope_type == "group":
bound["group_id"] = origin.scope_id
bound.pop("user_id", None)
else:
bound.pop("user_id", None)
bound.pop("group_id", None)
return bound
Loading
Loading