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 the @oc-hydra and @oc-internal-ai groups. 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
- The reporting user and the @oc-hydra and @oc-internal-ai groups 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_FEEDBACK_CC_USERGROUPS = [ 728 "S0BJ4K3LC4X", # @oc-hydra 729 "S0BKR63VAN5", # @oc-internal-ai 730] 731 732_TRIAGE_REPO_OWNER = "airbytehq" 733 734_TRIAGE_REPO_NAME = "airbyte-ops-mcp" 735 736_TRIAGE_WORKFLOW_FILE = "devin-session-triage.yml" 737 738_TRIAGE_DEFAULT_BRANCH = "main" 739 740_AI_SKILLS_REPO_URL = "https://github.com/airbytehq/ai-skills" 741 742_INTERNAL_SKILLS_URL = ( 743 "https://internal.airbyte.ai/docs/internal-docs/ai-engineering/skills" 744) 745 746_PLAYBOOK_ID_PATTERN = re.compile(r"^[a-z0-9_-]+$") 747 748_SKILL_ID_PATTERN = re.compile(r"^[a-z0-9-]+$") 749 750_CATEGORY_DISPLAY: dict[str, str] = { 751 "tool_failure": "Tool Failure", 752 "missing_guidance": "Missing Guidance", 753 "suspected_hallucination": "Suspected Hallucination", 754 "bad_approach": "Bad Approach", 755 "excessive_iteration": "Excessive Iteration", 756 "poor_quality": "Poor Quality", 757 "other_concern": "Other Concern", 758 "great_results": "Great Results", 759 "exceeded_expectations": "Exceeded Expectations", 760 "fast_completion": "Fast Completion", 761 "good_communication": "Good Communication", 762 "other_positive_feedback": "Other Positive Feedback", 763} 764 765 766class FeedbackCategory(StrEnum): 767 """Feedback categories for Devin session reports.""" 768 769 # Negative categories 770 TOOL_FAILURE = "tool_failure" 771 MISSING_GUIDANCE = "missing_guidance" 772 SUSPECTED_HALLUCINATION = "suspected_hallucination" 773 BAD_APPROACH = "bad_approach" 774 EXCESSIVE_ITERATION = "excessive_iteration" 775 POOR_QUALITY = "poor_quality" 776 OTHER_CONCERN = "other_concern" 777 778 # Positive categories 779 GREAT_RESULTS = "great_results" 780 EXCEEDED_EXPECTATIONS = "exceeded_expectations" 781 FAST_COMPLETION = "fast_completion" 782 GOOD_COMMUNICATION = "good_communication" 783 OTHER_POSITIVE_FEEDBACK = "other_positive_feedback" 784 785 def is_negative(self) -> bool: 786 """Return True if this is a negative feedback category.""" 787 return self in _NEGATIVE_MEMBERS 788 789 def display_name(self) -> str: 790 """Return the human-readable display name for this category.""" 791 return _CATEGORY_DISPLAY.get(self.value, self.value) 792 793 794_NEGATIVE_MEMBERS = frozenset( 795 { 796 FeedbackCategory.TOOL_FAILURE, 797 FeedbackCategory.MISSING_GUIDANCE, 798 FeedbackCategory.SUSPECTED_HALLUCINATION, 799 FeedbackCategory.BAD_APPROACH, 800 FeedbackCategory.EXCESSIVE_ITERATION, 801 FeedbackCategory.POOR_QUALITY, 802 FeedbackCategory.OTHER_CONCERN, 803 } 804) 805 806_SEVERITY_DISPLAY: dict[str, str] = { 807 "low": "Low", 808 "medium": "Medium", 809 "high": "High", 810 "critical": "Critical", 811} 812 813 814def _feedback_emoji(feedback_type: str) -> str: 815 """Return the header emoji for the given feedback type.""" 816 return ":tada:" if feedback_type == "positive" else ":warning:" 817 818 819def _feedback_label(feedback_type: str) -> str: 820 """Return the header label for the given feedback type.""" 821 type_display = "Positive" if feedback_type == "positive" else "Negative" 822 return f"Devin Session Feedback ({type_display})" 823 824 825def _format_playbook_link(playbook_id: str) -> str: 826 """Return Slack mrkdwn for a playbook identifier.""" 827 if playbook_id == "none": 828 return "none" 829 return f"<{_AI_SKILLS_REPO_URL}/blob/main/devin/playbooks/{playbook_id}.md|{playbook_id}>" 830 831 832def _format_skill_link(skill_id: str) -> str: 833 """Return Slack mrkdwn for a skill identifier.""" 834 return f"<{_INTERNAL_SKILLS_URL}/#{skill_id}|{skill_id}>" 835 836 837def _validate_playbook_id(playbook_id: str) -> str | None: 838 """Return an error message if `playbook_id` is not a valid playbook identifier.""" 839 if playbook_id == "none" or _PLAYBOOK_ID_PATTERN.fullmatch(playbook_id): 840 return None 841 return "session_playbook must be 'none' or a lowercase playbook ID using only letters, numbers, '-' and '_'." 842 843 844def _validate_skill_id(skill_id: str | None) -> str | None: 845 """Return an error message if `skill_id` is not a valid skill identifier.""" 846 if skill_id is None or _SKILL_ID_PATTERN.fullmatch(skill_id): 847 return None 848 return "related_skill_name must be a lowercase skill ID using only letters, numbers, and '-'." 849 850 851def _build_feedback_body( 852 *, 853 feedback_type: str, 854 category: str, 855 task_description: str, 856 session_playbook: str, 857 related_skill_name: str | None, 858 expected_behavior: str | None, 859 observed_behavior: str | None, 860 what_went_well: str | None, 861 severity: str | None, 862 steps_to_reproduce: str | None, 863) -> str: 864 """Build a Slack mrkdwn message body from structured feedback fields.""" 865 lines: list[str] = [] 866 867 cat = FeedbackCategory(category) 868 lines.append(f"*Category:* {cat.display_name()}") 869 870 if severity: 871 sev_display = _SEVERITY_DISPLAY.get(severity, severity) 872 lines.append(f"*Severity:* {sev_display}") 873 874 lines.append("") 875 lines.append(f"*Task:* {task_description}") 876 lines.append(f"*Session Playbook:* {_format_playbook_link(session_playbook)}") 877 if related_skill_name: 878 lines.append(f"*Related Skill:* {_format_skill_link(related_skill_name)}") 879 880 if feedback_type == "negative": 881 if expected_behavior: 882 lines.append("") 883 lines.append(f"*Expected Behavior:* {expected_behavior}") 884 if observed_behavior: 885 lines.append("") 886 lines.append(f"*Observed Behavior:* {observed_behavior}") 887 if steps_to_reproduce: 888 lines.append("") 889 lines.append(f"*Steps to Reproduce:* {steps_to_reproduce}") 890 else: 891 if what_went_well: 892 lines.append("") 893 lines.append(f"*What Went Well:* {what_went_well}") 894 895 if feedback_type == "negative": 896 lines.append("") 897 lines.append( 898 "_Auto-triage: a Devin session with v3 analyze mode will inspect this session._" 899 ) 900 901 return "\n".join(lines) 902 903 904def _validate_negative_fields( 905 expected_behavior: str | None, 906 observed_behavior: str | None, 907) -> str | None: 908 """Return an error message if required negative feedback fields are missing.""" 909 missing: list[str] = [] 910 if not expected_behavior: 911 missing.append("expected_behavior") 912 if not observed_behavior: 913 missing.append("observed_behavior") 914 if missing: 915 return f"Negative feedback requires: {', '.join(missing)}." 916 return None 917 918 919def _validate_positive_fields( 920 what_went_well: str | None, 921) -> str | None: 922 """Return an error message if required positive feedback fields are missing.""" 923 if not what_went_well: 924 return "Positive feedback requires: what_went_well." 925 return None 926 927 928def _dispatch_triage_workflow( 929 session_url: str, 930 feedback_context: str, 931 reporting_user: str, 932 session_playbook: str, 933 related_skill_name: str | None = None, 934 cc_persons: str = "", 935 header_emoji: str = "", 936 header_label: str = "", 937) -> WorkflowDispatchResult | None: 938 """Dispatch the v3 session triage workflow. 939 940 The triage workflow launches a Devin session with v3 analyze mode and 941 posts a single Slack notification via the HITL reusable workflow. 942 Formatting params (emoji, header, cc) are passed through to the HITL 943 notification so the caller doesn't need to post separately. 944 945 Returns the dispatch result, or None if dispatch fails. 946 """ 947 token = resolve_ci_trigger_github_token() 948 inputs: dict[str, str] = { 949 "session_url": session_url, 950 "feedback_context": feedback_context, 951 "reporting_user": reporting_user, 952 "session_playbook": session_playbook, 953 } 954 if related_skill_name: 955 inputs["related_skill_name"] = related_skill_name 956 if cc_persons: 957 inputs["cc_persons"] = cc_persons 958 if header_emoji: 959 inputs["header_emoji"] = header_emoji 960 if header_label: 961 inputs["header_label"] = header_label 962 try: 963 return trigger_workflow_dispatch( 964 owner=_TRIAGE_REPO_OWNER, 965 repo=_TRIAGE_REPO_NAME, 966 workflow_file=_TRIAGE_WORKFLOW_FILE, 967 ref=resolve_default_workflow_branch(_TRIAGE_DEFAULT_BRANCH), 968 inputs=inputs, 969 token=token, 970 ) 971 except requests.HTTPError: 972 logger.exception("Failed to dispatch triage workflow") 973 return None 974 975 976class SessionFeedbackResponse(BaseModel): 977 """Response from the session feedback tool.""" 978 979 success: bool = Field(description="Whether the workflow was triggered successfully") 980 message: str = Field(description="Human-readable status message") 981 workflow_url: str | None = Field( 982 default=None, 983 description="URL to view the GitHub Actions workflow file", 984 ) 985 run_id: int | None = Field( 986 default=None, 987 description="GitHub Actions workflow run ID", 988 ) 989 run_url: str | None = Field( 990 default=None, 991 description="Direct URL to the GitHub Actions workflow run", 992 ) 993 triage_run_url: str | None = Field( 994 default=None, 995 description="URL to the auto-triage workflow run", 996 ) 997 998 999@mcp_tool( 1000 read_only=False, 1001 idempotent=False, 1002 open_world=True, 1003) 1004def devin_session_feedback( 1005 feedback_type: Annotated[ 1006 Literal["positive", "negative"], 1007 Field( 1008 description=( 1009 "Type of feedback: 'positive' for a good experience or 'negative' for a " 1010 "bad experience. Use 'positive' when the user expresses satisfaction, " 1011 "praise, or a success story. Use 'negative' when the user reports a problem, " 1012 "frustration, or failure." 1013 ), 1014 ), 1015 ], 1016 category: Annotated[ 1017 FeedbackCategory, 1018 Field( 1019 description=( 1020 "Feedback category. " 1021 "For NEGATIVE feedback, use one of: " 1022 "'tool_failure' (a specific tool/integration broke), " 1023 "'missing_guidance' (Devin lacked instructions or context), " 1024 "'suspected_hallucination' (Devin fabricated information or made incorrect claims), " 1025 "'bad_approach' (Devin took a fundamentally wrong strategy), " 1026 "'excessive_iteration' (too many loops/retries before success), " 1027 "'poor_quality' (output quality below expectations), " 1028 "'other_concern'. " 1029 "For POSITIVE feedback, use one of: " 1030 "'great_results' (task completed with high quality), " 1031 "'exceeded_expectations' (went above and beyond), " 1032 "'fast_completion' (completed quickly and efficiently), " 1033 "'good_communication' (kept user well-informed), " 1034 "'other_positive_feedback'." 1035 ), 1036 ), 1037 ], 1038 task_description: Annotated[ 1039 str, 1040 Field( 1041 description=( 1042 "Brief description of what the user asked Devin to do. " 1043 "This sets the context for the feedback." 1044 ), 1045 ), 1046 ], 1047 agent_session_url: Annotated[ 1048 str, 1049 Field( 1050 description=( 1051 "Your agent session URL so the team can view the full context. " 1052 "Use the session URL from your system prompt." 1053 ), 1054 ), 1055 ], 1056 reporting_user: Annotated[ 1057 str, 1058 Field( 1059 description=( 1060 "The person providing the feedback. Accepts an email address " 1061 "(e.g. 'aj@airbyte.io'), a GitHub handle prefixed with @ " 1062 "(e.g. '@aaronsteers'), or a Slack user ID (e.g. 'U05AKF1BCC9')." 1063 ), 1064 ), 1065 ], 1066 session_playbook: Annotated[ 1067 str, 1068 Field( 1069 description=( 1070 "ID of the Devin playbook associated with the session (e.g. " 1071 "'devin_feedback_triage'), or 'none' when no playbook is associated. " 1072 "Required so feedback can identify whether playbook instructions may need updates." 1073 ), 1074 ), 1075 ], 1076 related_skill_name: Annotated[ 1077 str | None, 1078 Field( 1079 default=None, 1080 description=( 1081 "Optional skill ID associated with the feedback (e.g. " 1082 "'delete-declarative-source-def') when a related skill may need updates " 1083 "or is suspected of having issues." 1084 ), 1085 ), 1086 ], 1087 expected_behavior: Annotated[ 1088 str | None, 1089 Field( 1090 default=None, 1091 description=( 1092 "What should have happened. REQUIRED for negative feedback. " 1093 "Describe the expected outcome clearly." 1094 ), 1095 ), 1096 ], 1097 observed_behavior: Annotated[ 1098 str | None, 1099 Field( 1100 default=None, 1101 description=( 1102 "What actually happened. REQUIRED for negative feedback. " 1103 "Describe the actual outcome, including any error messages or unexpected results." 1104 ), 1105 ), 1106 ], 1107 what_went_well: Annotated[ 1108 str | None, 1109 Field( 1110 default=None, 1111 description=( 1112 "What specifically was good about the experience. REQUIRED for positive feedback. " 1113 "Be specific about what Devin did well." 1114 ), 1115 ), 1116 ], 1117 severity: Annotated[ 1118 Literal["low", "medium", "high", "critical"] | None, 1119 Field( 1120 default=None, 1121 description=( 1122 "Severity of the issue. Recommended for negative feedback. " 1123 "'low' = minor inconvenience, 'medium' = notable impact, " 1124 "'high' = significant blocker, 'critical' = complete failure." 1125 ), 1126 ), 1127 ], 1128 steps_to_reproduce: Annotated[ 1129 str | None, 1130 Field( 1131 default=None, 1132 description=( 1133 "Optional steps to reproduce the issue. Helpful for negative feedback " 1134 "to enable the team to investigate." 1135 ), 1136 ), 1137 ], 1138 session_to_evaluate: Annotated[ 1139 str | None, 1140 Field( 1141 default=None, 1142 description=( 1143 "Optional Devin session URL to evaluate/triage. Use this when reporting " 1144 "feedback about a *different* session (not your own). If omitted, " 1145 "agent_session_url is used as the session to triage (i.e., the reporter " 1146 "is reporting on itself)." 1147 ), 1148 ), 1149 ], 1150) -> SessionFeedbackResponse: 1151 """Report structured feedback about a Devin session experience via Slack. 1152 1153 Posts a formatted feedback message to the #hydra-feedback Slack channel, 1154 tagging the reporting user and the @oc-hydra and @oc-internal-ai groups. 1155 The message includes a clickable 1156 button for the Devin session link. For negative feedback, a triage workflow 1157 is automatically dispatched to launch a Devin session with v3 analyze mode 1158 that can inspect the original session's full conversation history. 1159 1160 IMPORTANT: This feedback will be logged publicly in Slack. Inform the user 1161 that their feedback is visible to the team and they may be contacted for 1162 additional details. 1163 1164 Use this tool when a user explicitly asks to report a positive or negative 1165 experience with their Devin session. Before calling this tool, let the user 1166 know: 1167 - Their feedback will be posted publicly in the #hydra-feedback Slack channel 1168 - They may be contacted by the team for more details 1169 - The reporting user and the @oc-hydra and @oc-internal-ai groups will be tagged in the message 1170 - For negative feedback, a triage session will be automatically launched to inspect the reported session 1171 1172 The Slack message is sent by a GitHub Actions workflow so that Slack 1173 credentials are never exposed to the calling agent. 1174 """ 1175 # Validate category matches feedback type. 1176 cat = FeedbackCategory(category) 1177 is_negative_feedback = feedback_type == "negative" 1178 id_validation_error = _validate_playbook_id(session_playbook) or _validate_skill_id( 1179 related_skill_name 1180 ) 1181 if id_validation_error: 1182 return SessionFeedbackResponse( 1183 success=False, 1184 message=id_validation_error, 1185 ) 1186 1187 if cat.is_negative() != is_negative_feedback: 1188 expected_kind = "negative" if is_negative_feedback else "positive" 1189 valid = [ 1190 c.value for c in FeedbackCategory if c.is_negative() == is_negative_feedback 1191 ] 1192 return SessionFeedbackResponse( 1193 success=False, 1194 message=( 1195 f"Invalid category '{category}' for {feedback_type} feedback. " 1196 f"Valid {expected_kind} categories: {', '.join(valid)}." 1197 ), 1198 ) 1199 1200 # Validate required fields based on feedback type. 1201 if feedback_type == "negative": 1202 validation_error = _validate_negative_fields( 1203 expected_behavior=expected_behavior, 1204 observed_behavior=observed_behavior, 1205 ) 1206 else: 1207 validation_error = _validate_positive_fields( 1208 what_went_well=what_went_well, 1209 ) 1210 1211 if validation_error: 1212 return SessionFeedbackResponse( 1213 success=False, 1214 message=validation_error, 1215 ) 1216 1217 message_body = _build_feedback_body( 1218 feedback_type=feedback_type, 1219 category=category, 1220 task_description=task_description, 1221 session_playbook=session_playbook, 1222 related_skill_name=related_skill_name, 1223 expected_behavior=expected_behavior, 1224 observed_behavior=observed_behavior, 1225 what_went_well=what_went_well, 1226 severity=severity, 1227 steps_to_reproduce=steps_to_reproduce, 1228 ) 1229 1230 # For negative feedback, dispatch triage workflow (which also posts to Slack 1231 # via the HITL reusable workflow — single message with triage button). 1232 # For positive feedback, dispatch HITL directly (no triage needed). 1233 if is_negative_feedback: 1234 triage_session_url = session_to_evaluate or agent_session_url 1235 # _dispatch_triage_workflow catches exceptions internally and returns None 1236 triage_result = _dispatch_triage_workflow( 1237 session_url=triage_session_url, 1238 feedback_context=message_body, 1239 reporting_user=reporting_user, 1240 session_playbook=session_playbook, 1241 related_skill_name=related_skill_name, 1242 cc_persons=",".join(_FEEDBACK_CC_USERGROUPS), 1243 header_emoji=_feedback_emoji(feedback_type), 1244 header_label=_feedback_label(feedback_type), 1245 ) 1246 if triage_result is not None: 1247 view_url = triage_result.run_url or triage_result.workflow_url 1248 return SessionFeedbackResponse( 1249 success=True, 1250 message=( 1251 "Feedback submitted. Auto-triage workflow launched. " 1252 "A Slack notification will be posted to #hydra-feedback " 1253 "once the triage session starts. " 1254 f"View workflow progress at: {view_url}" 1255 ), 1256 workflow_url=triage_result.workflow_url, 1257 run_id=triage_result.run_id, 1258 run_url=triage_result.run_url, 1259 triage_run_url=view_url, 1260 ) 1261 # Triage dispatch failed — fall back to direct HITL notification 1262 # so negative feedback is still recorded in Slack. 1263 logger.warning( 1264 "Triage workflow dispatch failed; falling back to direct HITL dispatch." 1265 ) 1266 1267 # Positive feedback (or negative feedback fallback): dispatch HITL directly 1268 result = dispatch_escalation( 1269 target_person=reporting_user, 1270 message=message_body, 1271 agent_session_url=agent_session_url, 1272 cc=list(_FEEDBACK_CC_USERGROUPS), 1273 channel_override=_FEEDBACK_CHANNEL, 1274 header_emoji=_feedback_emoji(feedback_type), 1275 header_label=_feedback_label(feedback_type), 1276 ) 1277 1278 view_url = result.run_url or result.workflow_url 1279 return SessionFeedbackResponse( 1280 success=True, 1281 message=( 1282 f"Feedback submitted and posted to #hydra-feedback. " 1283 f"The reporting user and the @oc-hydra and @oc-internal-ai groups " 1284 f"have been tagged. " 1285 f"View progress at: {view_url}" 1286 ), 1287 workflow_url=result.workflow_url, 1288 run_id=result.run_id, 1289 run_url=result.run_url, 1290 ) 1291 1292 1293_FOLLOWUP_HEADER = "🤖 *Automated Triage Update*" 1294 1295_FOLLOWUP_FOOTER_TEMPLATE = ( 1296 "_ℹ️ This thread is not monitored by Devin. " 1297 "Replies here will not be seen by any agent. " 1298 "For follow-up, use the <{agent_session_url}|linked session> or create a new task._" 1299) 1300 1301 1302def _wrap_followup_message(message: str, *, agent_session_url: str) -> str: 1303 """Wrap a follow-up message with session link and non-interactive disclaimer.""" 1304 footer = _FOLLOWUP_FOOTER_TEMPLATE.format(agent_session_url=agent_session_url) 1305 return f"{_FOLLOWUP_HEADER}\n\n{message}\n\n{footer}" 1306 1307 1308class SessionFeedbackFollowupResponse(BaseModel): 1309 """Response from the session feedback follow-up tool.""" 1310 1311 success: bool = Field(description="Whether the follow-up was posted successfully") 1312 message: str = Field(description="Human-readable status message") 1313 reply_ts: str | None = Field( 1314 default=None, 1315 description="Timestamp of the posted reply (Slack ts format)", 1316 ) 1317 1318 1319@mcp_tool( 1320 read_only=False, 1321 idempotent=False, 1322 open_world=True, 1323) 1324def devin_session_feedback_followup( 1325 thread_url: Annotated[ 1326 str, 1327 Field( 1328 description=( 1329 "Slack thread URL from the original feedback post in #hydra-feedback. " 1330 "This is the thread where follow-up context will be appended. " 1331 "Example: https://airbytehq-team.slack.com/archives/C0ACUHRP6B1/p1773062711122019" 1332 ), 1333 ), 1334 ], 1335 message: Annotated[ 1336 str, 1337 Field( 1338 description=( 1339 "Follow-up message text in Slack mrkdwn format. " 1340 "Typically a triage report or additional context about the " 1341 "feedback being investigated. " 1342 "Supports *bold*, _italic_, `code`, ```code blocks```, " 1343 "> blockquotes, and <url|label> links." 1344 ), 1345 ), 1346 ], 1347 agent_session_url: Annotated[ 1348 str, 1349 Field( 1350 description=( 1351 "Your agent session URL for audit trail. " 1352 "Use the session URL from your system prompt." 1353 ), 1354 ), 1355 ], 1356) -> SessionFeedbackFollowupResponse: 1357 """Post a follow-up to an existing feedback thread in #hydra-feedback. 1358 1359 This is the "second call" in the feedback workflow: after 1360 `devin_session_feedback` creates the initial report, this tool appends 1361 triage findings or additional context as a threaded reply. 1362 1363 Each reply is wrapped with a disclaimer clarifying that the thread is 1364 non-interactive and not monitored by any agent. 1365 1366 Workspace validation ensures only URLs from the expected Slack 1367 workspace are accepted. 1368 """ 1369 try: 1370 channel_id, thread_ts = parse_slack_thread_url(thread_url) 1371 except SlackURLParseError as exc: 1372 return SessionFeedbackFollowupResponse( 1373 success=False, 1374 message=str(exc), 1375 ) 1376 1377 wrapped_message = _wrap_followup_message( 1378 message, agent_session_url=agent_session_url 1379 ) 1380 1381 try: 1382 result = post_thread_reply( 1383 channel_id=channel_id, 1384 thread_ts=thread_ts, 1385 message=wrapped_message, 1386 ) 1387 reply_ts = result.ts 1388 except SlackAPIError as exc: 1389 return SessionFeedbackFollowupResponse( 1390 success=False, 1391 message=f"Slack API error: {exc}", 1392 ) 1393 1394 logger.info( 1395 "Feedback follow-up posted: channel=%s thread_ts=%s agent=%s", 1396 channel_id, 1397 thread_ts, 1398 agent_session_url, 1399 ) 1400 return SessionFeedbackFollowupResponse( 1401 success=True, 1402 message=f"Follow-up posted to feedback thread in channel {channel_id}.", 1403 reply_ts=reply_ts, 1404 ) 1405 1406 1407class DevinSessionNameResponse(BaseModel): 1408 """Response from the Devin session naming tool.""" 1409 1410 session_id: str = Field(description="The input session ID") 1411 scheme_version: str = Field(description="The naming scheme version identifier") 1412 name: str = Field( 1413 description="The generated human-friendly session name in Title Case" 1414 ) 1415 full_name: str = Field( 1416 description="The contextual full name including 'Devin' suffix (e.g. 'Silly Fred Devin')" 1417 ) 1418 1419 1420@mcp_tool( 1421 read_only=True, 1422 idempotent=True, 1423) 1424def get_devin_session_name( 1425 session_id: Annotated[ 1426 str, 1427 "The Devin session identifier or session URL. Accepts a raw session " 1428 "ID (e.g. 'b2a641e838214f91b50d0f88940ac119') or a full session URL " 1429 "(e.g. 'https://app.devin.ai/sessions/b2a641e8...'). The ID is " 1430 "extracted automatically from URLs. The same ID always produces " 1431 "the same name — this is a deterministic lookup, not a creation.", 1432 ], 1433) -> DevinSessionNameResponse: 1434 """Look up the deterministic friendly name for a Devin session. 1435 1436 Uses the silly-buddy naming scheme to generate a Title Case two-word 1437 name (e.g. "Smelly Fred") from the session ID. The output is immutable 1438 and idempotent — the same session ID always yields the same name. 1439 1440 If a full URL is provided instead of a bare ID, the session ID is 1441 extracted from the URL automatically. 1442 """ 1443 resolved_id = extract_session_id(session_id) 1444 scheme = NamingScheme.SILLY_BUDDY 1445 name = generate_friendly_name(resolved_id, scheme) 1446 full_name = f"{name} Devin" 1447 return DevinSessionNameResponse( 1448 session_id=resolved_id, 1449 scheme_version="v1", 1450 name=name, 1451 full_name=full_name, 1452 ) 1453 1454 1455def register_devin_ops_tools(app: FastMCP) -> None: 1456 """Register devin_ops tools with the FastMCP app.""" 1457 register_mcp_tools(app, mcp_module=__name__)