airbyte_ops_mcp.mcp.devin_ops
MCP tools for Devin agent-session operations: reminders, on-demand secret requests, session feedback, and session naming.
MCP reference
MCP primitives registered by the devin_ops module of the airbyte-internal-ops server: 7 tool(s), 0 prompt(s), 0 resource(s).
Tools (7)
cancel_devin_reminder
Hints: open-world
Cancel pending Devin reminders by session URL and specific GUIDs.
Removes matching reminders so they will not fire. Use this when instructed to stop reminders, or when a reminder is no longer needed.
Both agent_session_url and cancel_guids are required. Only reminders matching the session URL AND present in the GUID list are cancelled.
Parameters:
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
agent_session_url |
string |
yes | — | Your Devin session URL. Use the session URL from your system prompt. Required together with cancel_guids. |
cancel_guids |
array<string> |
yes | — | List of reminder GUIDs to cancel. You can get GUIDs from the reminder creation response or from the reminders list. |
Show input JSON schema
{
"additionalProperties": false,
"properties": {
"agent_session_url": {
"description": "Your Devin session URL. Use the session URL from your system prompt. Required together with cancel_guids.",
"type": "string"
},
"cancel_guids": {
"description": "List of reminder GUIDs to cancel. You can get GUIDs from the reminder creation response or from the reminders list.",
"items": {
"type": "string"
},
"type": "array"
}
},
"required": [
"agent_session_url",
"cancel_guids"
],
"type": "object"
}
Show output JSON schema
{
"description": "Response from the cancel_devin_reminder tool.",
"properties": {
"success": {
"description": "Whether the cancel workflow was triggered successfully",
"type": "boolean"
},
"message": {
"description": "Human-readable status message",
"type": "string"
},
"workflow_url": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "URL to view the GitHub Actions workflow file"
},
"run_id": {
"anyOf": [
{
"type": "integer"
},
{
"type": "null"
}
],
"default": null,
"description": "GitHub Actions workflow run ID"
},
"run_url": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "Direct URL to the GitHub Actions workflow run"
}
},
"required": [
"success",
"message"
],
"type": "object"
}
devin_session_feedback
Hints: open-world
Report structured feedback about a Devin session experience via Slack.
Posts a formatted feedback message to the #hydra-feedback Slack channel, tagging the reporting user and @AJ Steers. The message includes a clickable button for the Devin session link. For negative feedback, a triage workflow is automatically dispatched to launch a Devin session with v3 analyze mode that can inspect the original session's full conversation history.
IMPORTANT: This feedback will be logged publicly in Slack. Inform the user that their feedback is visible to the team and they may be contacted for additional details.
Use this tool when a user explicitly asks to report a positive or negative experience with their Devin session. Before calling this tool, let the user know:
- Their feedback will be posted publicly in the #hydra-feedback Slack channel
- They may be contacted by the team for more details
- Both the reporting user and @AJ Steers will be tagged in the message
- For negative feedback, a triage session will be automatically launched to inspect the reported session
The Slack message is sent by a GitHub Actions workflow so that Slack credentials are never exposed to the calling agent.
Parameters:
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
feedback_type |
enum("positive", "negative") |
yes | — | Type of feedback: 'positive' for a good experience or 'negative' for a bad experience. Use 'positive' when the user expresses satisfaction, praise, or a success story. Use 'negative' when the user reports a problem, frustration, or failure. |
category |
enum("tool_failure", "missing_guidance", "suspected_hallucination", "bad_approach", "excessive_iteration", "poor_quality", "other_concern", "great_results", "exceeded_expectations", "fast_completion", "good_communication", "other_positive_feedback") |
yes | — | Feedback category. For NEGATIVE feedback, use one of: 'tool_failure' (a specific tool/integration broke), 'missing_guidance' (Devin lacked instructions or context), 'suspected_hallucination' (Devin fabricated information or made incorrect claims), 'bad_approach' (Devin took a fundamentally wrong strategy), 'excessive_iteration' (too many loops/retries before success), 'poor_quality' (output quality below expectations), 'other_concern'. For POSITIVE feedback, use one of: 'great_results' (task completed with high quality), 'exceeded_expectations' (went above and beyond), 'fast_completion' (completed quickly and efficiently), 'good_communication' (kept user well-informed), 'other_positive_feedback'. |
task_description |
string |
yes | — | Brief description of what the user asked Devin to do. This sets the context for the feedback. |
agent_session_url |
string |
yes | — | Your agent session URL so the team can view the full context. Use the session URL from your system prompt. |
reporting_user |
string |
yes | — | The person providing the feedback. Accepts an email address (e.g. 'aj@airbyte.io'), a GitHub handle prefixed with @ (e.g. '@aaronsteers'), or a Slack user ID (e.g. 'U05AKF1BCC9'). |
session_playbook |
string |
yes | — | ID of the Devin playbook associated with the session (e.g. 'devin_feedback_triage'), or 'none' when no playbook is associated. Required so feedback can identify whether playbook instructions may need updates. |
related_skill_name |
string | null |
no | null |
Optional skill ID associated with the feedback (e.g. 'delete-declarative-source-def') when a related skill may need updates or is suspected of having issues. |
expected_behavior |
string | null |
no | null |
What should have happened. REQUIRED for negative feedback. Describe the expected outcome clearly. |
observed_behavior |
string | null |
no | null |
What actually happened. REQUIRED for negative feedback. Describe the actual outcome, including any error messages or unexpected results. |
what_went_well |
string | null |
no | null |
What specifically was good about the experience. REQUIRED for positive feedback. Be specific about what Devin did well. |
severity |
enum("low", "medium", "high", "critical") | null |
no | null |
Severity of the issue. Recommended for negative feedback. 'low' = minor inconvenience, 'medium' = notable impact, 'high' = significant blocker, 'critical' = complete failure. |
steps_to_reproduce |
string | null |
no | null |
Optional steps to reproduce the issue. Helpful for negative feedback to enable the team to investigate. |
session_to_evaluate |
string | null |
no | null |
Optional Devin session URL to evaluate/triage. Use this when reporting feedback about a different session (not your own). If omitted, agent_session_url is used as the session to triage (i.e., the reporter is reporting on itself). |
Show input JSON schema
{
"additionalProperties": false,
"properties": {
"feedback_type": {
"description": "Type of feedback: 'positive' for a good experience or 'negative' for a bad experience. Use 'positive' when the user expresses satisfaction, praise, or a success story. Use 'negative' when the user reports a problem, frustration, or failure.",
"enum": [
"positive",
"negative"
],
"type": "string"
},
"category": {
"description": "Feedback category. For NEGATIVE feedback, use one of: 'tool_failure' (a specific tool/integration broke), 'missing_guidance' (Devin lacked instructions or context), 'suspected_hallucination' (Devin fabricated information or made incorrect claims), 'bad_approach' (Devin took a fundamentally wrong strategy), 'excessive_iteration' (too many loops/retries before success), 'poor_quality' (output quality below expectations), 'other_concern'. For POSITIVE feedback, use one of: 'great_results' (task completed with high quality), 'exceeded_expectations' (went above and beyond), 'fast_completion' (completed quickly and efficiently), 'good_communication' (kept user well-informed), 'other_positive_feedback'.",
"enum": [
"tool_failure",
"missing_guidance",
"suspected_hallucination",
"bad_approach",
"excessive_iteration",
"poor_quality",
"other_concern",
"great_results",
"exceeded_expectations",
"fast_completion",
"good_communication",
"other_positive_feedback"
],
"type": "string"
},
"task_description": {
"description": "Brief description of what the user asked Devin to do. This sets the context for the feedback.",
"type": "string"
},
"agent_session_url": {
"description": "Your agent session URL so the team can view the full context. Use the session URL from your system prompt.",
"type": "string"
},
"reporting_user": {
"description": "The person providing the feedback. Accepts an email address (e.g. 'aj@airbyte.io'), a GitHub handle prefixed with @ (e.g. '@aaronsteers'), or a Slack user ID (e.g. 'U05AKF1BCC9').",
"type": "string"
},
"session_playbook": {
"description": "ID of the Devin playbook associated with the session (e.g. 'devin_feedback_triage'), or 'none' when no playbook is associated. Required so feedback can identify whether playbook instructions may need updates.",
"type": "string"
},
"related_skill_name": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "Optional skill ID associated with the feedback (e.g. 'delete-declarative-source-def') when a related skill may need updates or is suspected of having issues."
},
"expected_behavior": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "What should have happened. REQUIRED for negative feedback. Describe the expected outcome clearly."
},
"observed_behavior": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "What actually happened. REQUIRED for negative feedback. Describe the actual outcome, including any error messages or unexpected results."
},
"what_went_well": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "What specifically was good about the experience. REQUIRED for positive feedback. Be specific about what Devin did well."
},
"severity": {
"anyOf": [
{
"enum": [
"low",
"medium",
"high",
"critical"
],
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "Severity of the issue. Recommended for negative feedback. 'low' = minor inconvenience, 'medium' = notable impact, 'high' = significant blocker, 'critical' = complete failure."
},
"steps_to_reproduce": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "Optional steps to reproduce the issue. Helpful for negative feedback to enable the team to investigate."
},
"session_to_evaluate": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "Optional Devin session URL to evaluate/triage. Use this when reporting feedback about a *different* session (not your own). If omitted, agent_session_url is used as the session to triage (i.e., the reporter is reporting on itself)."
}
},
"required": [
"feedback_type",
"category",
"task_description",
"agent_session_url",
"reporting_user",
"session_playbook"
],
"type": "object"
}
Show output JSON schema
{
"description": "Response from the session feedback tool.",
"properties": {
"success": {
"description": "Whether the workflow was triggered successfully",
"type": "boolean"
},
"message": {
"description": "Human-readable status message",
"type": "string"
},
"workflow_url": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "URL to view the GitHub Actions workflow file"
},
"run_id": {
"anyOf": [
{
"type": "integer"
},
{
"type": "null"
}
],
"default": null,
"description": "GitHub Actions workflow run ID"
},
"run_url": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "Direct URL to the GitHub Actions workflow run"
},
"triage_run_url": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "URL to the auto-triage workflow run"
}
},
"required": [
"success",
"message"
],
"type": "object"
}
devin_session_feedback_followup
Hints: open-world
Post a follow-up to an existing feedback thread in #hydra-feedback.
This is the "second call" in the feedback workflow: after
devin_session_feedback creates the initial report, this tool appends
triage findings or additional context as a threaded reply.
Each reply is wrapped with a disclaimer clarifying that the thread is non-interactive and not monitored by any agent.
Workspace validation ensures only URLs from the expected Slack workspace are accepted.
Parameters:
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
thread_url |
string |
yes | — | Slack thread URL from the original feedback post in #hydra-feedback. This is the thread where follow-up context will be appended. Example: https://airbytehq-team.slack.com/archives/C0ACUHRP6B1/p1773062711122019 |
message |
string |
yes | — | Follow-up message text in Slack mrkdwn format. Typically a triage report or additional context about the feedback being investigated. Supports bold, _italic_, code, code blocks, > blockquotes, and |
agent_session_url |
string |
yes | — | Your agent session URL for audit trail. Use the session URL from your system prompt. |
Show input JSON schema
{
"additionalProperties": false,
"properties": {
"thread_url": {
"description": "Slack thread URL from the original feedback post in #hydra-feedback. This is the thread where follow-up context will be appended. Example: https://airbytehq-team.slack.com/archives/C0ACUHRP6B1/p1773062711122019",
"type": "string"
},
"message": {
"description": "Follow-up message text in Slack mrkdwn format. Typically a triage report or additional context about the feedback being investigated. Supports *bold*, _italic_, `code`, ```code blocks```, > blockquotes, and <url|label> links.",
"type": "string"
},
"agent_session_url": {
"description": "Your agent session URL for audit trail. Use the session URL from your system prompt.",
"type": "string"
}
},
"required": [
"thread_url",
"message",
"agent_session_url"
],
"type": "object"
}
Show output JSON schema
{
"description": "Response from the session feedback follow-up tool.",
"properties": {
"success": {
"description": "Whether the follow-up was posted successfully",
"type": "boolean"
},
"message": {
"description": "Human-readable status message",
"type": "string"
},
"reply_ts": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "Timestamp of the posted reply (Slack ts format)"
}
},
"required": [
"success",
"message"
],
"type": "object"
}
get_devin_session_name
Hints: read-only · idempotent
Look up the deterministic friendly name for a Devin session.
Uses the silly-buddy naming scheme to generate a Title Case two-word name (e.g. "Smelly Fred") from the session ID. The output is immutable and idempotent — the same session ID always yields the same name.
If a full URL is provided instead of a bare ID, the session ID is extracted from the URL automatically.
Parameters:
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
session_id |
string |
yes | — | The Devin session identifier or session URL. Accepts a raw session ID (e.g. 'b2a641e838214f91b50d0f88940ac119') or a full session URL (e.g. 'https://app.devin.ai/sessions/b2a641e8...'). The ID is extracted automatically from URLs. The same ID always produces the same name — this is a deterministic lookup, not a creation. |
Show input JSON schema
{
"additionalProperties": false,
"properties": {
"session_id": {
"description": "The Devin session identifier or session URL. Accepts a raw session ID (e.g. 'b2a641e838214f91b50d0f88940ac119') or a full session URL (e.g. 'https://app.devin.ai/sessions/b2a641e8...'). The ID is extracted automatically from URLs. The same ID always produces the same name \u2014 this is a deterministic lookup, not a creation.",
"type": "string"
}
},
"required": [
"session_id"
],
"type": "object"
}
Show output JSON schema
{
"description": "Response from the Devin session naming tool.",
"properties": {
"session_id": {
"description": "The input session ID",
"type": "string"
},
"scheme_version": {
"description": "The naming scheme version identifier",
"type": "string"
},
"name": {
"description": "The generated human-friendly session name in Title Case",
"type": "string"
},
"full_name": {
"description": "The contextual full name including 'Devin' suffix (e.g. 'Silly Fred Devin')",
"type": "string"
}
},
"required": [
"session_id",
"scheme_version",
"name",
"full_name"
],
"type": "object"
}
list_devin_secrets
Hints: open-world
List all available secret names in the 1Password vault.
Returns the sorted list of item titles from the 'devin-on-demand-secrets' vault. Use this to discover valid secret aliases before calling request_devin_secret.
This dispatches a GitHub Actions workflow (which has the 1Password credentials), waits for it to complete, then reads the list from the job logs.
Parameters:
_No parameters._
Show input JSON schema
{
"additionalProperties": false,
"properties": {},
"type": "object"
}
Show output JSON schema
{
"description": "Response from the list_devin_secrets tool.",
"properties": {
"success": {
"description": "Whether the operation succeeded",
"type": "boolean"
},
"message": {
"description": "Human-readable status message",
"type": "string"
},
"available_secrets": {
"description": "Sorted list of available secret names in the vault",
"items": {
"type": "string"
},
"type": "array"
}
},
"required": [
"success",
"message"
],
"type": "object"
}
request_devin_secret
Hints: open-world
Request a secret on demand via an approval workflow.
This tool operates in two phases:
Phase 1 (no approval_evidence_url): Dispatches a GitHub Actions workflow that validates the secret name against the 1Password vault and, if valid, sends a Slack approval request. If the secret name is not found, returns immediately with the list of available secret names so you can correct any typos.
Phase 2 (with approval_evidence_url): After a human approves the request, call this tool again with the approval evidence URL. This triggers a GitHub Actions workflow that reads the secret from 1Password and sends you a time-limited share link. Open the link in your browser to view and copy the secret.
Typical workflow:
- (Optional) Call list_devin_secrets first to see available names.
- Call this tool without approval_evidence_url to request approval.
- Note the
request_idin the response. - Wait for a human to approve the request in Slack.
- Obtain the approval evidence URL (Slack approval record URL).
- Call this tool again with the approval_evidence_url and the request_id from step 2.
- You will receive a 1Password share link -- open it in your browser to view and copy the secret values.
Parameters:
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
secret_alias |
string |
yes | — | The name of the secret to request. This must exactly match an item title in the 'devin-on-demand-secrets' 1Password vault. |
session_url |
string |
yes | — | Your Devin session URL (e.g. 'https://app.devin.ai/sessions/abc123...'). Use the session URL from your system prompt. |
approval_evidence_url |
string | null |
no | null |
Slack approval record URL (https:// |
target_approver |
string | null |
no | null |
Person to notify for approval (GitHub handle, email, or Slack user ID). Required for Phase 1 (approval request). |
request_id |
string | null |
no | null |
Request ID returned by Phase 1. Pass it back in Phase 2 so the approval record can be validated against the original request. Leave empty for Phase 1. |
Show input JSON schema
{
"additionalProperties": false,
"properties": {
"secret_alias": {
"description": "The name of the secret to request. This must exactly match an item title in the 'devin-on-demand-secrets' 1Password vault.",
"type": "string"
},
"session_url": {
"description": "Your Devin session URL (e.g. 'https://app.devin.ai/sessions/abc123...'). Use the session URL from your system prompt.",
"type": "string"
},
"approval_evidence_url": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "Slack approval record URL (https://<workspace>.slack.com/archives/...). Leave empty for Phase 1 (requesting approval). Provide the Slack URL for Phase 2 (delivering the secret after approval)."
},
"target_approver": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "Person to notify for approval (GitHub handle, email, or Slack user ID). Required for Phase 1 (approval request)."
},
"request_id": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "Request ID returned by Phase 1. Pass it back in Phase 2 so the approval record can be validated against the original request. Leave empty for Phase 1."
}
},
"required": [
"secret_alias",
"session_url"
],
"type": "object"
}
Show output JSON schema
{
"description": "Response from the request_devin_secret tool.",
"properties": {
"success": {
"description": "Whether the operation succeeded",
"type": "boolean"
},
"phase": {
"description": "Current phase: 'approval_requested' (Phase 1) or 'delivery_dispatched' (Phase 2)",
"type": "string"
},
"message": {
"description": "Human-readable status message",
"type": "string"
},
"slack_channel_url": {
"default": "https://airbytehq-team.slack.com/archives/C0AEXV81Q7N",
"description": "Direct URL to the #human-in-the-loop Slack channel",
"type": "string"
},
"secret_alias": {
"description": "The requested secret alias",
"type": "string"
},
"session_id": {
"description": "The Devin session ID",
"type": "string"
},
"workflow_url": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "URL to the GitHub Actions workflow"
},
"run_id": {
"anyOf": [
{
"type": "integer"
},
{
"type": "null"
}
],
"default": null,
"description": "GitHub Actions workflow run ID"
},
"run_url": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "Direct URL to the GitHub Actions workflow run"
},
"request_id": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "Unique request identifier (UUID). Returned in Phase 1; pass it back in Phase 2 for replay-protection validation."
}
},
"required": [
"success",
"phase",
"message",
"secret_alias",
"session_id"
],
"type": "object"
}
set_devin_reminder
Hints: open-world
Schedule a reminder that fires at a specified time or after a delay.
Creates a reminder that will be delivered back to your Devin session and posted to the #devin-reminders Slack channel when the time arrives. Reminders are checked every 30 minutes via a cron schedule.
Exactly one of delay_minutes or remind_at_local_time must be provided.
Prefer remind_at_local_time (Pacific local time) over delay_minutes
to avoid timezone-conversion mistakes — unless the user explicitly
asks for a reminder in N minutes.
The reminder is stored as a GitHub Actions artifact and processed by the devin-reminders-action. When the reminder is due, it injects a message into the originating Devin session and sends a Slack notification.
Use this tool when you need to schedule a follow-up action, check on a long-running process, or remind yourself about a task.
Parameters:
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
reminder_message |
string |
yes | — | The reminder message to deliver. Should clearly describe what you need to be reminded about. |
agent_session_url |
string |
yes | — | Your Devin session URL so the reminder can be injected back into your session. Use the session URL from your system prompt. |
delay_minutes |
integer | null |
no | null |
Number of minutes until the reminder fires. Must be a positive multiple of 30, up to 10080 (7 days). Examples: 30, 60, 120, 1440. Mutually exclusive with remind_at_local_time. |
remind_at_local_time |
string | null |
no | null |
Date-time in local time when the reminder should fire. At Airbyte, local time is always Pacific (America/Los_Angeles). Accepts '2026-04-02 09:00' (24-hour), '2026-04-02 9:00 AM' (12-hour), or ISO-like 'YYYY-MM-DDTHH:MM'. Must be in the future and within 7 days. Mutually exclusive with delay_minutes. PREFERRED — use this instead of delay_minutes to avoid timezone-conversion errors. |
slack_users_cc |
string | null |
no | null |
Optional comma-delimited list of Slack user tags to CC on the reminder notification. Example: '<@U12345>, <@U67890>'. |
Show input JSON schema
{
"additionalProperties": false,
"properties": {
"reminder_message": {
"description": "The reminder message to deliver. Should clearly describe what you need to be reminded about.",
"type": "string"
},
"agent_session_url": {
"description": "Your Devin session URL so the reminder can be injected back into your session. Use the session URL from your system prompt.",
"type": "string"
},
"delay_minutes": {
"anyOf": [
{
"type": "integer"
},
{
"type": "null"
}
],
"default": null,
"description": "Number of minutes until the reminder fires. Must be a positive multiple of 30, up to 10080 (7 days). Examples: 30, 60, 120, 1440. Mutually exclusive with remind_at_local_time."
},
"remind_at_local_time": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "Date-time in local time when the reminder should fire. At Airbyte, local time is always Pacific (America/Los_Angeles). Accepts '2026-04-02 09:00' (24-hour), '2026-04-02 9:00 AM' (12-hour), or ISO-like 'YYYY-MM-DDTHH:MM'. Must be in the future and within 7 days. Mutually exclusive with delay_minutes. PREFERRED \u2014 use this instead of delay_minutes to avoid timezone-conversion errors."
},
"slack_users_cc": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "Optional comma-delimited list of Slack user tags to CC on the reminder notification. Example: '<@U12345>, <@U67890>'."
}
},
"required": [
"reminder_message",
"agent_session_url"
],
"type": "object"
}
Show output JSON schema
{
"description": "Response from the set_devin_reminder tool.",
"properties": {
"success": {
"description": "Whether the workflow was triggered successfully",
"type": "boolean"
},
"message": {
"description": "Human-readable status message",
"type": "string"
},
"workflow_url": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "URL to view the GitHub Actions workflow file"
},
"run_id": {
"anyOf": [
{
"type": "integer"
},
{
"type": "null"
}
],
"default": null,
"description": "GitHub Actions workflow run ID"
},
"run_url": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "Direct URL to the GitHub Actions workflow run"
}
},
"required": [
"success",
"message"
],
"type": "object"
}
1# Copyright (c) 2025 Airbyte, Inc., all rights reserved. 2"""MCP tools for Devin agent-session operations: reminders, on-demand secret requests, session feedback, and session naming. 3 4## MCP reference 5 6.. include:: ../../../docs/mcp-generated/devin_ops.md 7 :start-line: 2 8""" 9 10# NOTE: We intentionally do NOT use `from __future__ import annotations` here. 11# FastMCP has issues resolving forward references when PEP 563 deferred annotations 12# are used. See: https://github.com/jlowin/fastmcp/issues/905 13# Python 3.12+ supports modern type hint syntax natively, so this is not needed. 14 15__all__: list[str] = [] 16 17import json 18import logging 19import re 20from enum import StrEnum 21from typing import Annotated, Literal 22 23import requests 24from fastmcp import FastMCP 25from fastmcp_extensions import mcp_tool, register_mcp_tools 26from pydantic import BaseModel, Field 27 28from airbyte_ops_mcp.devin_reminders import dispatch_cancel_reminder, dispatch_reminder 29from airbyte_ops_mcp.github_actions import ( 30 WorkflowDispatchResult, 31 download_job_logs, 32 get_workflow_jobs, 33 resolve_default_workflow_branch, 34 trigger_workflow_dispatch, 35 wait_for_workflow_completion, 36) 37from airbyte_ops_mcp.github_api import resolve_ci_trigger_github_token 38from airbyte_ops_mcp.human_in_the_loop import ( 39 HITL_SLACK_CHANNEL_URL, 40 dispatch_escalation, 41) 42from airbyte_ops_mcp.session_namer import ( 43 NamingScheme, 44 extract_session_id, 45 generate_friendly_name, 46) 47from airbyte_ops_mcp.slack_api import SlackAPIError, SlackURLParseError 48from airbyte_ops_mcp.slack_posting import parse_slack_thread_url, post_thread_reply 49 50 51class SetDevinReminderResponse(BaseModel): 52 """Response from the set_devin_reminder tool.""" 53 54 success: bool = Field(description="Whether the workflow was triggered successfully") 55 message: str = Field(description="Human-readable status message") 56 workflow_url: str | None = Field( 57 default=None, 58 description="URL to view the GitHub Actions workflow file", 59 ) 60 run_id: int | None = Field( 61 default=None, 62 description="GitHub Actions workflow run ID", 63 ) 64 run_url: str | None = Field( 65 default=None, 66 description="Direct URL to the GitHub Actions workflow run", 67 ) 68 69 70@mcp_tool( 71 read_only=False, 72 idempotent=False, 73 open_world=True, 74) 75def set_devin_reminder( 76 reminder_message: Annotated[ 77 str, 78 "The reminder message to deliver. Should clearly describe what " 79 "you need to be reminded about.", 80 ], 81 agent_session_url: Annotated[ 82 str, 83 "Your Devin session URL so the reminder can be injected back into " 84 "your session. Use the session URL from your system prompt.", 85 ], 86 delay_minutes: Annotated[ 87 int | None, 88 "Number of minutes until the reminder fires. Must be a positive " 89 "multiple of 30, up to 10080 (7 days). Examples: 30, 60, 120, 1440. " 90 "Mutually exclusive with remind_at_local_time.", 91 ] = None, 92 remind_at_local_time: Annotated[ 93 str | None, 94 "Date-time in local time when the reminder should fire. " 95 "At Airbyte, local time is always Pacific (America/Los_Angeles). " 96 "Accepts '2026-04-02 09:00' (24-hour), " 97 "'2026-04-02 9:00 AM' (12-hour), or ISO-like 'YYYY-MM-DDTHH:MM'. " 98 "Must be in the future and within 7 days. " 99 "Mutually exclusive with delay_minutes. " 100 "PREFERRED — use this instead of delay_minutes to avoid " 101 "timezone-conversion errors.", 102 ] = None, 103 slack_users_cc: Annotated[ 104 str | None, 105 "Optional comma-delimited list of Slack user tags to CC on the " 106 "reminder notification. Example: '<@U12345>, <@U67890>'.", 107 ] = None, 108) -> SetDevinReminderResponse: 109 """Schedule a reminder that fires at a specified time or after a delay. 110 111 Creates a reminder that will be delivered back to your Devin session 112 and posted to the #devin-reminders Slack channel when the time arrives. 113 Reminders are checked every 30 minutes via a cron schedule. 114 115 Exactly one of `delay_minutes` or `remind_at_local_time` must be provided. 116 Prefer `remind_at_local_time` (Pacific local time) over `delay_minutes` 117 to avoid timezone-conversion mistakes — unless the user explicitly 118 asks for a reminder in N minutes. 119 120 The reminder is stored as a GitHub Actions artifact and processed by 121 the devin-reminders-action. When the reminder is due, it injects a 122 message into the originating Devin session and sends a Slack notification. 123 124 Use this tool when you need to schedule a follow-up action, check on 125 a long-running process, or remind yourself about a task. 126 """ 127 try: 128 result = dispatch_reminder( 129 delay_minutes=delay_minutes, 130 remind_at_local_time=remind_at_local_time, 131 reminder_message=reminder_message, 132 agent_session_url=agent_session_url, 133 slack_users_cc=slack_users_cc, 134 ) 135 except ValueError as e: 136 return SetDevinReminderResponse( 137 success=False, 138 message=f"Invalid input: {e}", 139 ) 140 141 if remind_at_local_time: 142 time_desc = f"at {remind_at_local_time} Pacific" 143 else: 144 time_desc = f"in {delay_minutes} minutes" 145 146 view_url = result.run_url or result.workflow_url 147 return SetDevinReminderResponse( 148 success=True, 149 message=( 150 f"Reminder scheduled to fire {time_desc}. " 151 f"View progress at: {view_url}\n\n" 152 f"To cancel pending reminders for this session, use the " 153 f"`cancel_devin_reminder` tool." 154 ), 155 workflow_url=result.workflow_url, 156 run_id=result.run_id, 157 run_url=result.run_url, 158 ) 159 160 161class CancelDevinReminderResponse(BaseModel): 162 """Response from the cancel_devin_reminder tool.""" 163 164 success: bool = Field( 165 description="Whether the cancel workflow was triggered successfully" 166 ) 167 message: str = Field(description="Human-readable status message") 168 workflow_url: str | None = Field( 169 default=None, 170 description="URL to view the GitHub Actions workflow file", 171 ) 172 run_id: int | None = Field( 173 default=None, 174 description="GitHub Actions workflow run ID", 175 ) 176 run_url: str | None = Field( 177 default=None, 178 description="Direct URL to the GitHub Actions workflow run", 179 ) 180 181 182@mcp_tool( 183 read_only=False, 184 idempotent=False, 185 open_world=True, 186) 187def cancel_devin_reminder( 188 agent_session_url: Annotated[ 189 str, 190 "Your Devin session URL. Use the session URL from your system prompt. " 191 "Required together with cancel_guids.", 192 ], 193 cancel_guids: Annotated[ 194 list[str], 195 "List of reminder GUIDs to cancel. You can get GUIDs from " 196 "the reminder creation response or from the reminders list.", 197 ], 198) -> CancelDevinReminderResponse: 199 """Cancel pending Devin reminders by session URL and specific GUIDs. 200 201 Removes matching reminders so they will not fire. Use this when instructed 202 to stop reminders, or when a reminder is no longer needed. 203 204 Both agent_session_url and cancel_guids are required. Only reminders 205 matching the session URL AND present in the GUID list are cancelled. 206 """ 207 try: 208 result = dispatch_cancel_reminder( 209 agent_session_url=agent_session_url, 210 cancel_guids=cancel_guids, 211 ) 212 except ValueError as e: 213 return CancelDevinReminderResponse( 214 success=False, 215 message=f"Invalid input: {e}", 216 ) 217 218 view_url = result.run_url or result.workflow_url 219 guid_list = ", ".join(cancel_guids) 220 return CancelDevinReminderResponse( 221 success=True, 222 message=( 223 f"Cancel workflow triggered for GUIDs [{guid_list}] " 224 f"in session {agent_session_url}. View progress at: {view_url}" 225 ), 226 workflow_url=result.workflow_url, 227 run_id=result.run_id, 228 run_url=result.run_url, 229 ) 230 231 232logger = logging.getLogger(__name__) 233 234WORKFLOW_REPO_OWNER = "airbytehq" 235 236WORKFLOW_REPO_NAME = "airbyte-ops-mcp" 237 238WORKFLOW_FILE = "devin-secret-request.yml" 239 240WORKFLOW_DEFAULT_BRANCH = "main" 241 242_SESSION_ID_PATTERN = re.compile(r"[0-9a-fA-F]{32}") 243 244 245class SecretListResponse(BaseModel): 246 """Response from the list_devin_secrets tool.""" 247 248 success: bool = Field(description="Whether the operation succeeded") 249 message: str = Field(description="Human-readable status message") 250 available_secrets: list[str] = Field( 251 default_factory=list, 252 description="Sorted list of available secret names in the vault", 253 ) 254 255 256class SecretRequestResponse(BaseModel): 257 """Response from the request_devin_secret tool.""" 258 259 success: bool = Field(description="Whether the operation succeeded") 260 phase: str = Field( 261 description=( 262 "Current phase: 'approval_requested' (Phase 1) or " 263 "'delivery_dispatched' (Phase 2)" 264 ), 265 ) 266 message: str = Field(description="Human-readable status message") 267 slack_channel_url: str = Field( 268 default=HITL_SLACK_CHANNEL_URL, 269 description="Direct URL to the #human-in-the-loop Slack channel", 270 ) 271 secret_alias: str = Field(description="The requested secret alias") 272 session_id: str = Field(description="The Devin session ID") 273 workflow_url: str | None = Field( 274 default=None, 275 description="URL to the GitHub Actions workflow", 276 ) 277 run_id: int | None = Field( 278 default=None, 279 description="GitHub Actions workflow run ID", 280 ) 281 run_url: str | None = Field( 282 default=None, 283 description="Direct URL to the GitHub Actions workflow run", 284 ) 285 request_id: str | None = Field( 286 default=None, 287 description=( 288 "Unique request identifier (UUID). Returned in Phase 1; " 289 "pass it back in Phase 2 for replay-protection validation." 290 ), 291 ) 292 293 294@mcp_tool( 295 read_only=False, 296 idempotent=False, 297 open_world=True, 298) 299def list_devin_secrets() -> SecretListResponse: 300 """List all available secret names in the 1Password vault. 301 302 Returns the sorted list of item titles from the 303 'devin-on-demand-secrets' vault. Use this to discover valid 304 secret aliases before calling request_devin_secret. 305 306 This dispatches a GitHub Actions workflow (which has the 307 1Password credentials), waits for it to complete, then reads 308 the list from the job logs. 309 """ 310 return _list_secrets_via_workflow() 311 312 313@mcp_tool( 314 read_only=False, 315 idempotent=False, 316 open_world=True, 317) 318def request_devin_secret( 319 secret_alias: Annotated[ 320 str, 321 "The name of the secret to request. This must exactly match an item " 322 "title in the 'devin-on-demand-secrets' 1Password vault.", 323 ], 324 session_url: Annotated[ 325 str, 326 "Your Devin session URL (e.g. 'https://app.devin.ai/sessions/abc123...'). " 327 "Use the session URL from your system prompt.", 328 ], 329 approval_evidence_url: Annotated[ 330 str | None, 331 "Slack approval record URL " 332 "(https://<workspace>.slack.com/archives/...). " 333 "Leave empty for Phase 1 (requesting approval). Provide the " 334 "Slack URL for Phase 2 (delivering the secret after approval).", 335 ] = None, 336 target_approver: Annotated[ 337 str | None, 338 "Person to notify for approval (GitHub handle, email, or Slack user ID). " 339 "Required for Phase 1 (approval request).", 340 ] = None, 341 request_id: Annotated[ 342 str | None, 343 "Request ID returned by Phase 1. Pass it back in Phase 2 " 344 "so the approval record can be validated against the original request. " 345 "Leave empty for Phase 1.", 346 ] = None, 347) -> SecretRequestResponse: 348 """Request a secret on demand via an approval workflow. 349 350 This tool operates in two phases: 351 352 **Phase 1** (no approval_evidence_url): Dispatches a GitHub Actions 353 workflow that validates the secret name against the 1Password vault 354 and, if valid, sends a Slack approval request. If the secret name is 355 not found, returns immediately with the list of available secret 356 names so you can correct any typos. 357 358 **Phase 2** (with approval_evidence_url): After a human approves the 359 request, call this tool again with the approval evidence URL. This 360 triggers a GitHub Actions workflow that reads the secret from 361 1Password and sends you a time-limited share link. 362 Open the link in your browser to view and copy the secret. 363 364 Typical workflow: 365 0. (Optional) Call list_devin_secrets first to see available names. 366 1. Call this tool without approval_evidence_url to request approval. 367 2. Note the `request_id` in the response. 368 3. Wait for a human to approve the request in Slack. 369 4. Obtain the approval evidence URL (Slack approval record URL). 370 5. Call this tool again with the approval_evidence_url **and** the 371 request_id from step 2. 372 6. You will receive a 1Password share link -- open it in your 373 browser to view and copy the secret values. 374 """ 375 # Extract session ID from URL 376 match = _SESSION_ID_PATTERN.search(session_url) 377 if not match: 378 return SecretRequestResponse( 379 success=False, 380 phase="error", 381 message=( 382 f"No valid session ID found in URL: {session_url}. " 383 "Expected a 32-character hex string." 384 ), 385 secret_alias=secret_alias, 386 session_id="", 387 ) 388 session_id = match.group(0) 389 390 if not approval_evidence_url: 391 # Phase 1: Dispatch the request workflow (validates secret name 392 # inline using op CLI, then sends Slack approval if valid). 393 if not target_approver: 394 return SecretRequestResponse( 395 success=False, 396 phase="error", 397 message=( 398 "target_approver is required when requesting approval " 399 "(no approval_evidence_url provided)." 400 ), 401 secret_alias=secret_alias, 402 session_id=session_id, 403 ) 404 405 return _request_secret_via_workflow( 406 secret_alias=secret_alias, 407 session_id=session_id, 408 session_url=session_url, 409 target_approver=target_approver, 410 ) 411 412 # Phase 2: Deliver secret via GitHub Actions workflow 413 token = resolve_ci_trigger_github_token() 414 415 workflow_inputs: dict[str, str] = { 416 "action": "deliver", 417 "secret_alias": secret_alias, 418 "session_id": session_id, 419 "approval_evidence_url": approval_evidence_url, 420 } 421 if request_id: 422 workflow_inputs["expected_request_id"] = request_id 423 424 result = trigger_workflow_dispatch( 425 owner=WORKFLOW_REPO_OWNER, 426 repo=WORKFLOW_REPO_NAME, 427 workflow_file=WORKFLOW_FILE, 428 ref=resolve_default_workflow_branch(WORKFLOW_DEFAULT_BRANCH), 429 inputs=workflow_inputs, 430 token=token, 431 ) 432 433 view_url = result.run_url or result.workflow_url 434 return SecretRequestResponse( 435 success=True, 436 phase="delivery_dispatched", 437 message=( 438 f"Secret delivery workflow dispatched for '{secret_alias}'. " 439 f"The workflow will read the secret from 1Password and send " 440 f"you a time-limited share link. Once you receive the link, " 441 f"open it in your browser to view and copy the secret. " 442 f"View progress: {view_url}" 443 ), 444 secret_alias=secret_alias, 445 session_id=session_id, 446 workflow_url=result.workflow_url, 447 run_id=result.run_id, 448 run_url=result.run_url, 449 request_id=request_id, 450 ) 451 452 453def _request_secret_via_workflow( 454 secret_alias: str, 455 session_id: str, 456 session_url: str, 457 target_approver: str, 458) -> SecretRequestResponse: 459 """Dispatch the request workflow, wait, and parse the result from job logs. 460 461 The workflow validates the secret alias against the vault inline, 462 then sends the Slack approval if valid. On a bad alias the workflow 463 fails and the job logs contain a JSON object with `available_secrets`. 464 """ 465 token = resolve_ci_trigger_github_token() 466 467 dispatch_result = trigger_workflow_dispatch( 468 owner=WORKFLOW_REPO_OWNER, 469 repo=WORKFLOW_REPO_NAME, 470 workflow_file=WORKFLOW_FILE, 471 ref=resolve_default_workflow_branch(WORKFLOW_DEFAULT_BRANCH), 472 inputs={ 473 "action": "request", 474 "secret_alias": secret_alias, 475 "session_id": session_id, 476 "target_approver": target_approver, 477 }, 478 token=token, 479 ) 480 if not dispatch_result.run_id: 481 return SecretRequestResponse( 482 success=False, 483 phase="error", 484 message=( 485 "Workflow dispatched but no run ID returned. " 486 f"Check: {dispatch_result.workflow_url}" 487 ), 488 secret_alias=secret_alias, 489 session_id=session_id, 490 workflow_url=dispatch_result.workflow_url, 491 ) 492 493 run_status = wait_for_workflow_completion( 494 owner=WORKFLOW_REPO_OWNER, 495 repo=WORKFLOW_REPO_NAME, 496 run_id=dispatch_result.run_id, 497 token=token, 498 ) 499 500 # Download logs from the validation job (multi-job workflow) 501 raw_logs = _download_run_logs( 502 dispatch_result.run_id, token, job_name="Validate Secret Name" 503 ) 504 505 if run_status.succeeded: 506 # Parse the approval-requested JSON from the logs 507 result_data = _find_json_in_logs(raw_logs, "phase") if raw_logs else None 508 request_id = result_data.get("request_id") if result_data else None 509 view_url = run_status.run_url or dispatch_result.workflow_url 510 return SecretRequestResponse( 511 success=True, 512 phase="approval_requested", 513 message=( 514 f"Approval request for secret '{secret_alias}' sent to " 515 f"#human-in-the-loop ({HITL_SLACK_CHANNEL_URL}). " 516 f"Waiting for human approval. " 517 f"Once approved, call this tool again with the " 518 f"approval_evidence_url to deliver the secret. " 519 f"View progress: {view_url}" 520 ), 521 secret_alias=secret_alias, 522 session_id=session_id, 523 workflow_url=dispatch_result.workflow_url, 524 run_id=dispatch_result.run_id, 525 run_url=run_status.run_url, 526 request_id=request_id, 527 ) 528 529 # Workflow failed — check if it was a validation failure 530 error_data = _find_json_in_logs(raw_logs, "available_secrets") if raw_logs else None 531 if error_data: 532 available = error_data.get("available_secrets", []) 533 formatted = ", ".join(f"`{s}`" for s in available) 534 return SecretRequestResponse( 535 success=False, 536 phase="validation_failed", 537 message=( 538 f"Secret '{secret_alias}' not found in the vault. " 539 f"Available secrets: {formatted}" 540 ), 541 secret_alias=secret_alias, 542 session_id=session_id, 543 workflow_url=dispatch_result.workflow_url, 544 run_id=dispatch_result.run_id, 545 run_url=run_status.run_url, 546 ) 547 548 # Generic workflow failure 549 return SecretRequestResponse( 550 success=False, 551 phase="error", 552 message=( 553 f"Request workflow failed (conclusion={run_status.conclusion}). " 554 f"See: {run_status.run_url}" 555 ), 556 secret_alias=secret_alias, 557 session_id=session_id, 558 workflow_url=dispatch_result.workflow_url, 559 run_id=dispatch_result.run_id, 560 run_url=run_status.run_url, 561 ) 562 563 564def _download_run_logs( 565 run_id: int, 566 token: str, 567 *, 568 job_name: str | None = None, 569) -> str | None: 570 """Best-effort download of a job's logs for a workflow run. 571 572 Args: 573 run_id: GitHub Actions workflow run ID. 574 token: GitHub API token used for log download. Note: job listing 575 uses `get_workflow_jobs` which resolves its own token via 576 `resolve_ci_trigger_github_token()`. 577 job_name: If provided, find the job whose name contains this 578 substring (case-insensitive). Skipped jobs are always 579 excluded. Falls back to the first non-skipped job. 580 """ 581 try: 582 jobs = get_workflow_jobs( 583 owner=WORKFLOW_REPO_OWNER, 584 repo=WORKFLOW_REPO_NAME, 585 run_id=run_id, 586 ) 587 # Filter out skipped jobs (common in multi-job conditional workflows) 588 active_jobs = [j for j in jobs if j.conclusion != "skipped"] 589 if not active_jobs: 590 return None 591 592 target = active_jobs[0] # default: first non-skipped job 593 if job_name: 594 needle = job_name.lower() 595 for j in active_jobs: 596 if needle in j.name.lower(): 597 target = j 598 break 599 600 return download_job_logs( 601 owner=WORKFLOW_REPO_OWNER, 602 repo=WORKFLOW_REPO_NAME, 603 job_id=target.job_id, 604 token=token, 605 ) 606 except (requests.HTTPError, ValueError) as exc: 607 logger.warning("Failed to download job logs for run %s: %s", run_id, exc) 608 return None 609 610 611def _list_secrets_via_workflow() -> SecretListResponse: 612 """Dispatch the list workflow, wait for completion, and parse titles from job logs.""" 613 token = resolve_ci_trigger_github_token() 614 615 # 1. Dispatch the workflow with action="list" 616 dispatch_result = trigger_workflow_dispatch( 617 owner=WORKFLOW_REPO_OWNER, 618 repo=WORKFLOW_REPO_NAME, 619 workflow_file=WORKFLOW_FILE, 620 ref=resolve_default_workflow_branch(WORKFLOW_DEFAULT_BRANCH), 621 inputs={"action": "list", "session_id": "0" * 32}, 622 token=token, 623 ) 624 if not dispatch_result.run_id: 625 return SecretListResponse( 626 success=False, 627 message=( 628 "Workflow dispatched but no run ID returned. " 629 f"Check: {dispatch_result.workflow_url}" 630 ), 631 ) 632 633 # 2. Wait for the workflow to complete 634 run_status = wait_for_workflow_completion( 635 owner=WORKFLOW_REPO_OWNER, 636 repo=WORKFLOW_REPO_NAME, 637 run_id=dispatch_result.run_id, 638 token=token, 639 ) 640 if not run_status.succeeded: 641 return SecretListResponse( 642 success=False, 643 message=( 644 f"Workflow run failed (conclusion={run_status.conclusion}). " 645 f"See: {run_status.run_url}" 646 ), 647 ) 648 649 # 3. Find the job and download its logs 650 jobs = get_workflow_jobs( 651 owner=WORKFLOW_REPO_OWNER, 652 repo=WORKFLOW_REPO_NAME, 653 run_id=dispatch_result.run_id, 654 ) 655 if not jobs: 656 return SecretListResponse( 657 success=False, 658 message="Workflow completed but no jobs found.", 659 ) 660 661 # Find the list job (multi-job workflow; skip skipped jobs) 662 active_jobs = [j for j in jobs if j.conclusion != "skipped"] 663 if not active_jobs: 664 return SecretListResponse( 665 success=False, 666 message="Workflow completed but all jobs were skipped.", 667 ) 668 669 target_job = active_jobs[0] 670 for j in active_jobs: 671 if "list" in j.name.lower(): 672 target_job = j 673 break 674 675 raw_logs = download_job_logs( 676 owner=WORKFLOW_REPO_OWNER, 677 repo=WORKFLOW_REPO_NAME, 678 job_id=target_job.job_id, 679 token=token, 680 ) 681 682 # 4. Parse JSON output from the logs 683 data = _find_json_in_logs(raw_logs, "available_secrets") 684 if data is None: 685 return SecretListResponse( 686 success=False, 687 message=( 688 "Could not parse secret list from workflow logs. " 689 f"See: {run_status.run_url}" 690 ), 691 ) 692 693 secrets = data.get("available_secrets", []) 694 titles = [str(t) for t in secrets] if isinstance(secrets, list) else [] 695 return SecretListResponse( 696 success=True, 697 message=f"Found {len(titles)} available secrets in the vault.", 698 available_secrets=sorted(titles), 699 ) 700 701 702def _find_json_in_logs(raw_logs: str, required_key: str) -> dict | None: 703 """Find the first JSON object in job logs that contains *required_key*. 704 705 GitHub Actions job logs prefix each line with a timestamp. We scan 706 every line looking for a JSON object that contains the given key. 707 Returns the parsed dict, or `None` if not found. 708 """ 709 for line in raw_logs.splitlines(): 710 stripped = line.strip() 711 if not stripped.startswith("{"): 712 idx = stripped.find("{") 713 if idx < 0: 714 continue 715 stripped = stripped[idx:] 716 try: 717 data = json.loads(stripped) 718 except json.JSONDecodeError: 719 continue 720 if isinstance(data, dict) and required_key in data: 721 return data 722 return None 723 724 725_FEEDBACK_CHANNEL = "C0ACUHRP6B1" 726 727_AJ_STEERS_IDENTIFIER = "U05AKF1BCC9" 728 729_TRIAGE_REPO_OWNER = "airbytehq" 730 731_TRIAGE_REPO_NAME = "airbyte-ops-mcp" 732 733_TRIAGE_WORKFLOW_FILE = "devin-session-triage.yml" 734 735_TRIAGE_DEFAULT_BRANCH = "main" 736 737_AI_SKILLS_REPO_URL = "https://github.com/airbytehq/ai-skills" 738 739_INTERNAL_SKILLS_URL = ( 740 "https://internal.airbyte.ai/docs/internal-docs/ai-engineering/skills" 741) 742 743_PLAYBOOK_ID_PATTERN = re.compile(r"^[a-z0-9_-]+$") 744 745_SKILL_ID_PATTERN = re.compile(r"^[a-z0-9-]+$") 746 747_CATEGORY_DISPLAY: dict[str, str] = { 748 "tool_failure": "Tool Failure", 749 "missing_guidance": "Missing Guidance", 750 "suspected_hallucination": "Suspected Hallucination", 751 "bad_approach": "Bad Approach", 752 "excessive_iteration": "Excessive Iteration", 753 "poor_quality": "Poor Quality", 754 "other_concern": "Other Concern", 755 "great_results": "Great Results", 756 "exceeded_expectations": "Exceeded Expectations", 757 "fast_completion": "Fast Completion", 758 "good_communication": "Good Communication", 759 "other_positive_feedback": "Other Positive Feedback", 760} 761 762 763class FeedbackCategory(StrEnum): 764 """Feedback categories for Devin session reports.""" 765 766 # Negative categories 767 TOOL_FAILURE = "tool_failure" 768 MISSING_GUIDANCE = "missing_guidance" 769 SUSPECTED_HALLUCINATION = "suspected_hallucination" 770 BAD_APPROACH = "bad_approach" 771 EXCESSIVE_ITERATION = "excessive_iteration" 772 POOR_QUALITY = "poor_quality" 773 OTHER_CONCERN = "other_concern" 774 775 # Positive categories 776 GREAT_RESULTS = "great_results" 777 EXCEEDED_EXPECTATIONS = "exceeded_expectations" 778 FAST_COMPLETION = "fast_completion" 779 GOOD_COMMUNICATION = "good_communication" 780 OTHER_POSITIVE_FEEDBACK = "other_positive_feedback" 781 782 def is_negative(self) -> bool: 783 """Return True if this is a negative feedback category.""" 784 return self in _NEGATIVE_MEMBERS 785 786 def display_name(self) -> str: 787 """Return the human-readable display name for this category.""" 788 return _CATEGORY_DISPLAY.get(self.value, self.value) 789 790 791_NEGATIVE_MEMBERS = frozenset( 792 { 793 FeedbackCategory.TOOL_FAILURE, 794 FeedbackCategory.MISSING_GUIDANCE, 795 FeedbackCategory.SUSPECTED_HALLUCINATION, 796 FeedbackCategory.BAD_APPROACH, 797 FeedbackCategory.EXCESSIVE_ITERATION, 798 FeedbackCategory.POOR_QUALITY, 799 FeedbackCategory.OTHER_CONCERN, 800 } 801) 802 803_SEVERITY_DISPLAY: dict[str, str] = { 804 "low": "Low", 805 "medium": "Medium", 806 "high": "High", 807 "critical": "Critical", 808} 809 810 811def _feedback_emoji(feedback_type: str) -> str: 812 """Return the header emoji for the given feedback type.""" 813 return ":tada:" if feedback_type == "positive" else ":warning:" 814 815 816def _feedback_label(feedback_type: str) -> str: 817 """Return the header label for the given feedback type.""" 818 type_display = "Positive" if feedback_type == "positive" else "Negative" 819 return f"Devin Session Feedback ({type_display})" 820 821 822def _format_playbook_link(playbook_id: str) -> str: 823 """Return Slack mrkdwn for a playbook identifier.""" 824 if playbook_id == "none": 825 return "none" 826 return f"<{_AI_SKILLS_REPO_URL}/blob/main/devin/playbooks/{playbook_id}.md|{playbook_id}>" 827 828 829def _format_skill_link(skill_id: str) -> str: 830 """Return Slack mrkdwn for a skill identifier.""" 831 return f"<{_INTERNAL_SKILLS_URL}/#{skill_id}|{skill_id}>" 832 833 834def _validate_playbook_id(playbook_id: str) -> str | None: 835 """Return an error message if `playbook_id` is not a valid playbook identifier.""" 836 if playbook_id == "none" or _PLAYBOOK_ID_PATTERN.fullmatch(playbook_id): 837 return None 838 return "session_playbook must be 'none' or a lowercase playbook ID using only letters, numbers, '-' and '_'." 839 840 841def _validate_skill_id(skill_id: str | None) -> str | None: 842 """Return an error message if `skill_id` is not a valid skill identifier.""" 843 if skill_id is None or _SKILL_ID_PATTERN.fullmatch(skill_id): 844 return None 845 return "related_skill_name must be a lowercase skill ID using only letters, numbers, and '-'." 846 847 848def _build_feedback_body( 849 *, 850 feedback_type: str, 851 category: str, 852 task_description: str, 853 session_playbook: str, 854 related_skill_name: str | None, 855 expected_behavior: str | None, 856 observed_behavior: str | None, 857 what_went_well: str | None, 858 severity: str | None, 859 steps_to_reproduce: str | None, 860) -> str: 861 """Build a Slack mrkdwn message body from structured feedback fields.""" 862 lines: list[str] = [] 863 864 cat = FeedbackCategory(category) 865 lines.append(f"*Category:* {cat.display_name()}") 866 867 if severity: 868 sev_display = _SEVERITY_DISPLAY.get(severity, severity) 869 lines.append(f"*Severity:* {sev_display}") 870 871 lines.append("") 872 lines.append(f"*Task:* {task_description}") 873 lines.append(f"*Session Playbook:* {_format_playbook_link(session_playbook)}") 874 if related_skill_name: 875 lines.append(f"*Related Skill:* {_format_skill_link(related_skill_name)}") 876 877 if feedback_type == "negative": 878 if expected_behavior: 879 lines.append("") 880 lines.append(f"*Expected Behavior:* {expected_behavior}") 881 if observed_behavior: 882 lines.append("") 883 lines.append(f"*Observed Behavior:* {observed_behavior}") 884 if steps_to_reproduce: 885 lines.append("") 886 lines.append(f"*Steps to Reproduce:* {steps_to_reproduce}") 887 else: 888 if what_went_well: 889 lines.append("") 890 lines.append(f"*What Went Well:* {what_went_well}") 891 892 if feedback_type == "negative": 893 lines.append("") 894 lines.append( 895 "_Auto-triage: a Devin session with v3 analyze mode will inspect this session._" 896 ) 897 898 return "\n".join(lines) 899 900 901def _validate_negative_fields( 902 expected_behavior: str | None, 903 observed_behavior: str | None, 904) -> str | None: 905 """Return an error message if required negative feedback fields are missing.""" 906 missing: list[str] = [] 907 if not expected_behavior: 908 missing.append("expected_behavior") 909 if not observed_behavior: 910 missing.append("observed_behavior") 911 if missing: 912 return f"Negative feedback requires: {', '.join(missing)}." 913 return None 914 915 916def _validate_positive_fields( 917 what_went_well: str | None, 918) -> str | None: 919 """Return an error message if required positive feedback fields are missing.""" 920 if not what_went_well: 921 return "Positive feedback requires: what_went_well." 922 return None 923 924 925def _dispatch_triage_workflow( 926 session_url: str, 927 feedback_context: str, 928 reporting_user: str, 929 session_playbook: str, 930 related_skill_name: str | None = None, 931 cc_persons: str = "", 932 header_emoji: str = "", 933 header_label: str = "", 934) -> WorkflowDispatchResult | None: 935 """Dispatch the v3 session triage workflow. 936 937 The triage workflow launches a Devin session with v3 analyze mode and 938 posts a single Slack notification via the HITL reusable workflow. 939 Formatting params (emoji, header, cc) are passed through to the HITL 940 notification so the caller doesn't need to post separately. 941 942 Returns the dispatch result, or None if dispatch fails. 943 """ 944 token = resolve_ci_trigger_github_token() 945 inputs: dict[str, str] = { 946 "session_url": session_url, 947 "feedback_context": feedback_context, 948 "reporting_user": reporting_user, 949 "session_playbook": session_playbook, 950 } 951 if related_skill_name: 952 inputs["related_skill_name"] = related_skill_name 953 if cc_persons: 954 inputs["cc_persons"] = cc_persons 955 if header_emoji: 956 inputs["header_emoji"] = header_emoji 957 if header_label: 958 inputs["header_label"] = header_label 959 try: 960 return trigger_workflow_dispatch( 961 owner=_TRIAGE_REPO_OWNER, 962 repo=_TRIAGE_REPO_NAME, 963 workflow_file=_TRIAGE_WORKFLOW_FILE, 964 ref=resolve_default_workflow_branch(_TRIAGE_DEFAULT_BRANCH), 965 inputs=inputs, 966 token=token, 967 ) 968 except requests.HTTPError: 969 logger.exception("Failed to dispatch triage workflow") 970 return None 971 972 973class SessionFeedbackResponse(BaseModel): 974 """Response from the session feedback tool.""" 975 976 success: bool = Field(description="Whether the workflow was triggered successfully") 977 message: str = Field(description="Human-readable status message") 978 workflow_url: str | None = Field( 979 default=None, 980 description="URL to view the GitHub Actions workflow file", 981 ) 982 run_id: int | None = Field( 983 default=None, 984 description="GitHub Actions workflow run ID", 985 ) 986 run_url: str | None = Field( 987 default=None, 988 description="Direct URL to the GitHub Actions workflow run", 989 ) 990 triage_run_url: str | None = Field( 991 default=None, 992 description="URL to the auto-triage workflow run", 993 ) 994 995 996@mcp_tool( 997 read_only=False, 998 idempotent=False, 999 open_world=True, 1000) 1001def devin_session_feedback( 1002 feedback_type: Annotated[ 1003 Literal["positive", "negative"], 1004 Field( 1005 description=( 1006 "Type of feedback: 'positive' for a good experience or 'negative' for a " 1007 "bad experience. Use 'positive' when the user expresses satisfaction, " 1008 "praise, or a success story. Use 'negative' when the user reports a problem, " 1009 "frustration, or failure." 1010 ), 1011 ), 1012 ], 1013 category: Annotated[ 1014 FeedbackCategory, 1015 Field( 1016 description=( 1017 "Feedback category. " 1018 "For NEGATIVE feedback, use one of: " 1019 "'tool_failure' (a specific tool/integration broke), " 1020 "'missing_guidance' (Devin lacked instructions or context), " 1021 "'suspected_hallucination' (Devin fabricated information or made incorrect claims), " 1022 "'bad_approach' (Devin took a fundamentally wrong strategy), " 1023 "'excessive_iteration' (too many loops/retries before success), " 1024 "'poor_quality' (output quality below expectations), " 1025 "'other_concern'. " 1026 "For POSITIVE feedback, use one of: " 1027 "'great_results' (task completed with high quality), " 1028 "'exceeded_expectations' (went above and beyond), " 1029 "'fast_completion' (completed quickly and efficiently), " 1030 "'good_communication' (kept user well-informed), " 1031 "'other_positive_feedback'." 1032 ), 1033 ), 1034 ], 1035 task_description: Annotated[ 1036 str, 1037 Field( 1038 description=( 1039 "Brief description of what the user asked Devin to do. " 1040 "This sets the context for the feedback." 1041 ), 1042 ), 1043 ], 1044 agent_session_url: Annotated[ 1045 str, 1046 Field( 1047 description=( 1048 "Your agent session URL so the team can view the full context. " 1049 "Use the session URL from your system prompt." 1050 ), 1051 ), 1052 ], 1053 reporting_user: Annotated[ 1054 str, 1055 Field( 1056 description=( 1057 "The person providing the feedback. Accepts an email address " 1058 "(e.g. 'aj@airbyte.io'), a GitHub handle prefixed with @ " 1059 "(e.g. '@aaronsteers'), or a Slack user ID (e.g. 'U05AKF1BCC9')." 1060 ), 1061 ), 1062 ], 1063 session_playbook: Annotated[ 1064 str, 1065 Field( 1066 description=( 1067 "ID of the Devin playbook associated with the session (e.g. " 1068 "'devin_feedback_triage'), or 'none' when no playbook is associated. " 1069 "Required so feedback can identify whether playbook instructions may need updates." 1070 ), 1071 ), 1072 ], 1073 related_skill_name: Annotated[ 1074 str | None, 1075 Field( 1076 default=None, 1077 description=( 1078 "Optional skill ID associated with the feedback (e.g. " 1079 "'delete-declarative-source-def') when a related skill may need updates " 1080 "or is suspected of having issues." 1081 ), 1082 ), 1083 ], 1084 expected_behavior: Annotated[ 1085 str | None, 1086 Field( 1087 default=None, 1088 description=( 1089 "What should have happened. REQUIRED for negative feedback. " 1090 "Describe the expected outcome clearly." 1091 ), 1092 ), 1093 ], 1094 observed_behavior: Annotated[ 1095 str | None, 1096 Field( 1097 default=None, 1098 description=( 1099 "What actually happened. REQUIRED for negative feedback. " 1100 "Describe the actual outcome, including any error messages or unexpected results." 1101 ), 1102 ), 1103 ], 1104 what_went_well: Annotated[ 1105 str | None, 1106 Field( 1107 default=None, 1108 description=( 1109 "What specifically was good about the experience. REQUIRED for positive feedback. " 1110 "Be specific about what Devin did well." 1111 ), 1112 ), 1113 ], 1114 severity: Annotated[ 1115 Literal["low", "medium", "high", "critical"] | None, 1116 Field( 1117 default=None, 1118 description=( 1119 "Severity of the issue. Recommended for negative feedback. " 1120 "'low' = minor inconvenience, 'medium' = notable impact, " 1121 "'high' = significant blocker, 'critical' = complete failure." 1122 ), 1123 ), 1124 ], 1125 steps_to_reproduce: Annotated[ 1126 str | None, 1127 Field( 1128 default=None, 1129 description=( 1130 "Optional steps to reproduce the issue. Helpful for negative feedback " 1131 "to enable the team to investigate." 1132 ), 1133 ), 1134 ], 1135 session_to_evaluate: Annotated[ 1136 str | None, 1137 Field( 1138 default=None, 1139 description=( 1140 "Optional Devin session URL to evaluate/triage. Use this when reporting " 1141 "feedback about a *different* session (not your own). If omitted, " 1142 "agent_session_url is used as the session to triage (i.e., the reporter " 1143 "is reporting on itself)." 1144 ), 1145 ), 1146 ], 1147) -> SessionFeedbackResponse: 1148 """Report structured feedback about a Devin session experience via Slack. 1149 1150 Posts a formatted feedback message to the #hydra-feedback Slack channel, 1151 tagging the reporting user and @AJ Steers. The message includes a clickable 1152 button for the Devin session link. For negative feedback, a triage workflow 1153 is automatically dispatched to launch a Devin session with v3 analyze mode 1154 that can inspect the original session's full conversation history. 1155 1156 IMPORTANT: This feedback will be logged publicly in Slack. Inform the user 1157 that their feedback is visible to the team and they may be contacted for 1158 additional details. 1159 1160 Use this tool when a user explicitly asks to report a positive or negative 1161 experience with their Devin session. Before calling this tool, let the user 1162 know: 1163 - Their feedback will be posted publicly in the #hydra-feedback Slack channel 1164 - They may be contacted by the team for more details 1165 - Both the reporting user and @AJ Steers will be tagged in the message 1166 - For negative feedback, a triage session will be automatically launched to inspect the reported session 1167 1168 The Slack message is sent by a GitHub Actions workflow so that Slack 1169 credentials are never exposed to the calling agent. 1170 """ 1171 # Validate category matches feedback type. 1172 cat = FeedbackCategory(category) 1173 is_negative_feedback = feedback_type == "negative" 1174 id_validation_error = _validate_playbook_id(session_playbook) or _validate_skill_id( 1175 related_skill_name 1176 ) 1177 if id_validation_error: 1178 return SessionFeedbackResponse( 1179 success=False, 1180 message=id_validation_error, 1181 ) 1182 1183 if cat.is_negative() != is_negative_feedback: 1184 expected_kind = "negative" if is_negative_feedback else "positive" 1185 valid = [ 1186 c.value for c in FeedbackCategory if c.is_negative() == is_negative_feedback 1187 ] 1188 return SessionFeedbackResponse( 1189 success=False, 1190 message=( 1191 f"Invalid category '{category}' for {feedback_type} feedback. " 1192 f"Valid {expected_kind} categories: {', '.join(valid)}." 1193 ), 1194 ) 1195 1196 # Validate required fields based on feedback type. 1197 if feedback_type == "negative": 1198 validation_error = _validate_negative_fields( 1199 expected_behavior=expected_behavior, 1200 observed_behavior=observed_behavior, 1201 ) 1202 else: 1203 validation_error = _validate_positive_fields( 1204 what_went_well=what_went_well, 1205 ) 1206 1207 if validation_error: 1208 return SessionFeedbackResponse( 1209 success=False, 1210 message=validation_error, 1211 ) 1212 1213 message_body = _build_feedback_body( 1214 feedback_type=feedback_type, 1215 category=category, 1216 task_description=task_description, 1217 session_playbook=session_playbook, 1218 related_skill_name=related_skill_name, 1219 expected_behavior=expected_behavior, 1220 observed_behavior=observed_behavior, 1221 what_went_well=what_went_well, 1222 severity=severity, 1223 steps_to_reproduce=steps_to_reproduce, 1224 ) 1225 1226 # For negative feedback, dispatch triage workflow (which also posts to Slack 1227 # via the HITL reusable workflow — single message with triage button). 1228 # For positive feedback, dispatch HITL directly (no triage needed). 1229 if is_negative_feedback: 1230 triage_session_url = session_to_evaluate or agent_session_url 1231 # _dispatch_triage_workflow catches exceptions internally and returns None 1232 triage_result = _dispatch_triage_workflow( 1233 session_url=triage_session_url, 1234 feedback_context=message_body, 1235 reporting_user=reporting_user, 1236 session_playbook=session_playbook, 1237 related_skill_name=related_skill_name, 1238 cc_persons=_AJ_STEERS_IDENTIFIER, 1239 header_emoji=_feedback_emoji(feedback_type), 1240 header_label=_feedback_label(feedback_type), 1241 ) 1242 if triage_result is not None: 1243 view_url = triage_result.run_url or triage_result.workflow_url 1244 return SessionFeedbackResponse( 1245 success=True, 1246 message=( 1247 "Feedback submitted. Auto-triage workflow launched. " 1248 "A Slack notification will be posted to #hydra-feedback " 1249 "once the triage session starts. " 1250 f"View workflow progress at: {view_url}" 1251 ), 1252 workflow_url=triage_result.workflow_url, 1253 run_id=triage_result.run_id, 1254 run_url=triage_result.run_url, 1255 triage_run_url=view_url, 1256 ) 1257 # Triage dispatch failed — fall back to direct HITL notification 1258 # so negative feedback is still recorded in Slack. 1259 logger.warning( 1260 "Triage workflow dispatch failed; falling back to direct HITL dispatch." 1261 ) 1262 1263 # Positive feedback (or negative feedback fallback): dispatch HITL directly 1264 result = dispatch_escalation( 1265 target_person=reporting_user, 1266 message=message_body, 1267 agent_session_url=agent_session_url, 1268 cc=[_AJ_STEERS_IDENTIFIER], 1269 channel_override=_FEEDBACK_CHANNEL, 1270 header_emoji=_feedback_emoji(feedback_type), 1271 header_label=_feedback_label(feedback_type), 1272 ) 1273 1274 view_url = result.run_url or result.workflow_url 1275 return SessionFeedbackResponse( 1276 success=True, 1277 message=( 1278 f"Feedback submitted and posted to #hydra-feedback. " 1279 f"The reporting user and @AJ Steers have been tagged. " 1280 f"View progress at: {view_url}" 1281 ), 1282 workflow_url=result.workflow_url, 1283 run_id=result.run_id, 1284 run_url=result.run_url, 1285 ) 1286 1287 1288_FOLLOWUP_HEADER = "🤖 *Automated Triage Update*" 1289 1290_FOLLOWUP_FOOTER_TEMPLATE = ( 1291 "_ℹ️ This thread is not monitored by Devin. " 1292 "Replies here will not be seen by any agent. " 1293 "For follow-up, use the <{agent_session_url}|linked session> or create a new task._" 1294) 1295 1296 1297def _wrap_followup_message(message: str, *, agent_session_url: str) -> str: 1298 """Wrap a follow-up message with session link and non-interactive disclaimer.""" 1299 footer = _FOLLOWUP_FOOTER_TEMPLATE.format(agent_session_url=agent_session_url) 1300 return f"{_FOLLOWUP_HEADER}\n\n{message}\n\n{footer}" 1301 1302 1303class SessionFeedbackFollowupResponse(BaseModel): 1304 """Response from the session feedback follow-up tool.""" 1305 1306 success: bool = Field(description="Whether the follow-up was posted successfully") 1307 message: str = Field(description="Human-readable status message") 1308 reply_ts: str | None = Field( 1309 default=None, 1310 description="Timestamp of the posted reply (Slack ts format)", 1311 ) 1312 1313 1314@mcp_tool( 1315 read_only=False, 1316 idempotent=False, 1317 open_world=True, 1318) 1319def devin_session_feedback_followup( 1320 thread_url: Annotated[ 1321 str, 1322 Field( 1323 description=( 1324 "Slack thread URL from the original feedback post in #hydra-feedback. " 1325 "This is the thread where follow-up context will be appended. " 1326 "Example: https://airbytehq-team.slack.com/archives/C0ACUHRP6B1/p1773062711122019" 1327 ), 1328 ), 1329 ], 1330 message: Annotated[ 1331 str, 1332 Field( 1333 description=( 1334 "Follow-up message text in Slack mrkdwn format. " 1335 "Typically a triage report or additional context about the " 1336 "feedback being investigated. " 1337 "Supports *bold*, _italic_, `code`, ```code blocks```, " 1338 "> blockquotes, and <url|label> links." 1339 ), 1340 ), 1341 ], 1342 agent_session_url: Annotated[ 1343 str, 1344 Field( 1345 description=( 1346 "Your agent session URL for audit trail. " 1347 "Use the session URL from your system prompt." 1348 ), 1349 ), 1350 ], 1351) -> SessionFeedbackFollowupResponse: 1352 """Post a follow-up to an existing feedback thread in #hydra-feedback. 1353 1354 This is the "second call" in the feedback workflow: after 1355 `devin_session_feedback` creates the initial report, this tool appends 1356 triage findings or additional context as a threaded reply. 1357 1358 Each reply is wrapped with a disclaimer clarifying that the thread is 1359 non-interactive and not monitored by any agent. 1360 1361 Workspace validation ensures only URLs from the expected Slack 1362 workspace are accepted. 1363 """ 1364 try: 1365 channel_id, thread_ts = parse_slack_thread_url(thread_url) 1366 except SlackURLParseError as exc: 1367 return SessionFeedbackFollowupResponse( 1368 success=False, 1369 message=str(exc), 1370 ) 1371 1372 wrapped_message = _wrap_followup_message( 1373 message, agent_session_url=agent_session_url 1374 ) 1375 1376 try: 1377 result = post_thread_reply( 1378 channel_id=channel_id, 1379 thread_ts=thread_ts, 1380 message=wrapped_message, 1381 ) 1382 reply_ts = result.ts 1383 except SlackAPIError as exc: 1384 return SessionFeedbackFollowupResponse( 1385 success=False, 1386 message=f"Slack API error: {exc}", 1387 ) 1388 1389 logger.info( 1390 "Feedback follow-up posted: channel=%s thread_ts=%s agent=%s", 1391 channel_id, 1392 thread_ts, 1393 agent_session_url, 1394 ) 1395 return SessionFeedbackFollowupResponse( 1396 success=True, 1397 message=f"Follow-up posted to feedback thread in channel {channel_id}.", 1398 reply_ts=reply_ts, 1399 ) 1400 1401 1402class DevinSessionNameResponse(BaseModel): 1403 """Response from the Devin session naming tool.""" 1404 1405 session_id: str = Field(description="The input session ID") 1406 scheme_version: str = Field(description="The naming scheme version identifier") 1407 name: str = Field( 1408 description="The generated human-friendly session name in Title Case" 1409 ) 1410 full_name: str = Field( 1411 description="The contextual full name including 'Devin' suffix (e.g. 'Silly Fred Devin')" 1412 ) 1413 1414 1415@mcp_tool( 1416 read_only=True, 1417 idempotent=True, 1418) 1419def get_devin_session_name( 1420 session_id: Annotated[ 1421 str, 1422 "The Devin session identifier or session URL. Accepts a raw session " 1423 "ID (e.g. 'b2a641e838214f91b50d0f88940ac119') or a full session URL " 1424 "(e.g. 'https://app.devin.ai/sessions/b2a641e8...'). The ID is " 1425 "extracted automatically from URLs. The same ID always produces " 1426 "the same name — this is a deterministic lookup, not a creation.", 1427 ], 1428) -> DevinSessionNameResponse: 1429 """Look up the deterministic friendly name for a Devin session. 1430 1431 Uses the silly-buddy naming scheme to generate a Title Case two-word 1432 name (e.g. "Smelly Fred") from the session ID. The output is immutable 1433 and idempotent — the same session ID always yields the same name. 1434 1435 If a full URL is provided instead of a bare ID, the session ID is 1436 extracted from the URL automatically. 1437 """ 1438 resolved_id = extract_session_id(session_id) 1439 scheme = NamingScheme.SILLY_BUDDY 1440 name = generate_friendly_name(resolved_id, scheme) 1441 full_name = f"{name} Devin" 1442 return DevinSessionNameResponse( 1443 session_id=resolved_id, 1444 scheme_version="v1", 1445 name=name, 1446 full_name=full_name, 1447 ) 1448 1449 1450def register_devin_ops_tools(app: FastMCP) -> None: 1451 """Register devin_ops tools with the FastMCP app.""" 1452 register_mcp_tools(app, mcp_module=__name__)