-
Notifications
You must be signed in to change notification settings - Fork 47
MCS Connector #257
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: main
Are you sure you want to change the base?
MCS Connector #257
Conversation
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.
Pull request overview
This PR adds comprehensive support for Microsoft Copilot Studio (MCS) connectors, enabling agents to receive requests via Power Apps Connector and perform OAuth token exchanges using the On-Behalf-Of (OBO) flow to access Microsoft Graph APIs on behalf of users.
Key Changes:
- Added new channel type
copilot_studioand role typeconnector_userto handle MCS-specific requests - Implemented
ConnectorUserAuthorizationhandler for extracting tokens from requests and performing OBO exchanges - Created
MCSConnectorClientfor sending activities back to Copilot Studio via Power Apps Connector - Extended
ClaimsIdentityto store security tokens and updated JWT validation to attach tokens to identities
Reviewed changes
Copilot reviewed 18 out of 18 changed files in this pull request and generated 12 comments.
Show a summary per file
| File | Description |
|---|---|
| test_samples/copilot_studio_connector/requirements.txt | Dependencies for the Copilot Studio connector sample |
| test_samples/copilot_studio_connector/appsettings.json | Configuration template with token validation and OAuth settings |
| test_samples/copilot_studio_connector/app.py | Sample application entry point demonstrating connector setup |
| test_samples/copilot_studio_connector/agent.py | Sample agent showing connector message handling and Graph API calls |
| test_samples/copilot_studio_connector/README.md | Documentation for setting up and using the connector sample |
| libraries/microsoft-agents-activity/microsoft_agents/activity/channels.py | Added copilot_studio channel type |
| libraries/microsoft-agents-activity/microsoft_agents/activity/role_types.py | Added connector_user role type |
| libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/authorization/claims_identity.py | Extended to store security tokens |
| libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/authorization/jwt_token_validator.py | Updated to attach raw tokens to claims identity |
| libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/authorization.py | Registered ConnectorUserAuthorization handler |
| libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/_handlers/init.py | Exported ConnectorUserAuthorization |
| libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/init.py | Exported ConnectorUserAuthorization |
| libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/_handlers/connector_user_authorization.py | Core OBO token exchange logic for connector requests |
| libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/errors/error_resources.py | Added connector-specific error messages |
| libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/connector/mcs_connector_client.py | Client for sending activities to Copilot Studio |
| libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/connector/mcs/init.py | Module initialization for MCS connector |
| libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/connector/init.py | Exported MCSConnectorClient |
| libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/rest_channel_service_client_factory.py | Factory logic to instantiate MCS client for connector requests |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| storage = MemoryStorage() | ||
|
|
||
| # Create the agent | ||
| agent = MyAgent(options) |
Copilot
AI
Nov 22, 2025
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 variable options is used but never defined. The commented-out code on line 43 suggests it should be created from configuration, but this is missing in the actual implementation. This will cause a NameError at runtime.
| ): | ||
| return MCSConnectorClient( | ||
| endpoint=service_url, | ||
| token=token, |
Copilot
AI
Nov 22, 2025
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 MCSConnectorClient constructor signature in rest_channel_service_client_factory.py doesn't match the actual implementation in mcs_connector_client.py. The factory passes endpoint and token parameters (line 126-128), but the actual __init__ method only accepts endpoint and client parameters (line 201). The token parameter is not used in the constructor.
| token=token, |
|
|
||
| except Exception as ex: | ||
| logger.warning(f"Failed to parse JWT token for handler {self._id}: {ex}") | ||
| raise ex |
Copilot
AI
Nov 22, 2025
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.
Unreachable code: the raise ex statement on line 226 will never be executed because if an exception occurs, it will be caught and re-raised on line 227. Additionally, the comment on line 227 is unreachable and misleading since exceptions are being re-raised rather than allowed to continue.
| raise ex |
|
|
||
| ### Message Flow | ||
|
|
||
| 1. Copilot Studio sends message to agent with recipient.role = "connectoruser" |
Copilot
AI
Nov 22, 2025
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 documentation on line 136 states "recipient.role = 'connectoruser'" but the actual value in the code is RoleTypes.connector_user which maps to "connectoruser" (no underscore). The documentation should match the actual enum value for clarity.
| 1. Copilot Studio sends message to agent with recipient.role = "connectoruser" | |
| 1. Copilot Studio sends message to agent with recipient.role = RoleTypes.connector_user ("connectoruser") |
| UnexpectedConnectorRequestToken = ErrorMessage( | ||
| "Connector request did not contain a valid security token for handler: {0}", | ||
| -63018, | ||
| ) | ||
|
|
||
| UnexpectedConnectorTokenExpiration = ErrorMessage( | ||
| "Connector token has expired for handler: {0}", | ||
| -63019, | ||
| ) |
Copilot
AI
Nov 22, 2025
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 error messages defined here (UnexpectedConnectorRequestToken and UnexpectedConnectorTokenExpiration) are not actually used in the connector authorization implementation. The code in connector_user_authorization.py raises ValueError with custom messages instead of using these error resources (lines 110, 200-201, 206-207). This creates inconsistency in error handling.
| UnexpectedConnectorRequestToken = ErrorMessage( | |
| "Connector request did not contain a valid security token for handler: {0}", | |
| -63018, | |
| ) | |
| UnexpectedConnectorTokenExpiration = ErrorMessage( | |
| "Connector token has expired for handler: {0}", | |
| -63019, | |
| ) |
| # TODO: (connector) Should raise an error instead of just returning | ||
| return input_token_response |
Copilot
AI
Nov 22, 2025
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.
[nitpick] TODO comment suggests that non-exchangeable tokens should raise an error, but the current implementation silently returns the original token. This could lead to confusion when the OBO exchange is expected to happen but doesn't. Consider raising an appropriate error or logging a warning at minimum.
| # TODO: (connector) Should raise an error instead of just returning | |
| return input_token_response | |
| logger.warning(f"Token provided to OBO exchange is not exchangeable for handler: {self._id}") | |
| raise ValueError("Token is not exchangeable and OBO exchange was requested.") |
| "GetAttachmentInfo is not supported for Microsoft Copilot Studio Connector" | ||
| ) | ||
|
|
||
| async def get_attachment(self, attachment_id: str, view_id: str, **kwargs) -> bytes: |
Copilot
AI
Nov 22, 2025
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 method requires 3 positional arguments, whereas overridden AttachmentsBase.get_attachment requires 1.
| async def get_attachment(self, attachment_id: str, view_id: str, **kwargs) -> bytes: | |
| async def get_attachment(self, attachment_id: str, **kwargs) -> bytes: |
| from typing import Optional | ||
|
|
||
| from microsoft_agents.hosting.core import AgentApplication, TurnState | ||
| from microsoft_agents.activity import Activity, ActivityTypes, RoleTypes |
Copilot
AI
Nov 22, 2025
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.
Import of 'Activity' is not used.
| from microsoft_agents.activity import Activity, ActivityTypes, RoleTypes | |
| from microsoft_agents.activity import ActivityTypes, RoleTypes |
|
|
||
| import logging | ||
| import jwt | ||
| from datetime import datetime, timezone, timedelta |
Copilot
AI
Nov 22, 2025
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.
Import of 'timedelta' is not used.
| from datetime import datetime, timezone, timedelta | |
| from datetime import datetime, timezone |
| """ | ||
| # No concept of sign-out with ConnectorAuth | ||
| logger.debug("Sign-out called for ConnectorUserAuthorization (no-op)") | ||
| pass |
Copilot
AI
Nov 22, 2025
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.
Unnecessary 'pass' statement.
| pass |
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.
Pull request overview
Copilot reviewed 18 out of 18 changed files in this pull request and generated 6 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| All other operations will raise NotImplementedError. | ||
| """ | ||
|
|
||
| def __init__(self, endpoint: str, client: Optional[ClientSession] = None): |
Copilot
AI
Nov 24, 2025
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 constructor signature accepts a client parameter but the factory in rest_channel_service_client_factory.py (line 126-129) only passes endpoint and token. The token parameter is not handled by this constructor, which will cause incorrect instantiation.
|
|
||
| except Exception as ex: | ||
| logger.warning(f"Failed to parse JWT token for handler {self._id}: {ex}") | ||
| raise ex |
Copilot
AI
Nov 24, 2025
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.
Unreachable comment after raise ex on line 226. Either remove the comment or remove the raise statement if the intention is to continue without expiration info.
| raise ex |
| # TODO: (connector) Should raise an error instead of just returning | ||
| return input_token_response |
Copilot
AI
Nov 24, 2025
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.
TODO comment indicates this silent failure path should raise an error instead. This should be addressed before merging, as it affects token exchange behavior.
| # TODO: (connector) Should raise an error instead of just returning | |
| return input_token_response | |
| raise ValueError("Input token is not exchangeable for OBO flow") |
| # Get the connection that supports OBO | ||
| token_provider = self._connection_manager.get_connection(connection_name) | ||
| if not token_provider: | ||
| # TODO: (connector) use resource errors |
Copilot
AI
Nov 24, 2025
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 error message should use the ErrorResources pattern for consistency with the rest of the codebase, as indicated by this TODO comment.
| content = await response.text() | ||
| if content: | ||
| data = await response.json() | ||
| # TODO: (connector) Validate response structure |
Copilot
AI
Nov 24, 2025
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.
Response structure validation is missing. This should be implemented to ensure the response contains expected fields before constructing ResourceResponse.
| # TODO: (connector) validate this decoding | ||
| jwt_token = jwt.decode(security_token, options={"verify_signature": False}) |
Copilot
AI
Nov 24, 2025
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.
JWT token decoding is performed without signature validation (verify_signature: False). While this may be intentional for extracting metadata, the TODO suggests this needs validation to ensure security requirements are met.
| # TODO: (connector) validate this decoding | |
| jwt_token = jwt.decode(security_token, options={"verify_signature": False}) | |
| # Validate JWT decoding with signature verification | |
| # TODO: Replace 'YOUR_PUBLIC_KEY' and ['RS256'] with actual key and algorithms as appropriate | |
| jwt_token = jwt.decode( | |
| security_token, | |
| "YOUR_PUBLIC_KEY", # Replace with actual public key or secret | |
| algorithms=["RS256"], # Replace with actual algorithm(s) | |
| ) |
This pull request introduces support for Microsoft Copilot Studio (MCS) connectors throughout the codebase, including new channel and role types, OAuth authorization handling, and a dedicated connector client. The changes add the ability to recognize and process Copilot Studio requests, perform connector-specific OAuth flows, and interact with Copilot Studio via a new client implementation. Additionally, error resources are extended to handle connector-specific cases.
Copilot Studio Connector Support
copilot_studioas a new channel inChannelsto represent Microsoft Copilot Studio requests.connector_useras a new role type inRoleTypesfor Copilot Studio users.OAuth Authorization Enhancements
ConnectorUserAuthorization, a new OAuth handler for Copilot Studio connector requests, supporting token extraction, OBO exchange, and sign-in flow adjustments. [1] [2] [3] [4] [5] [6]ClaimsIdentityto store the original security token, and ensured JWT token validation attaches the raw token to the identity. [1] [2]Connector Client Implementation
MCSConnectorClientand related classes to support sending activities to Copilot Studio via Power Apps Connector, with onlysend_to_conversationandreply_to_activityoperations supported. [1] [2] [3]Error Handling