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

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
295 changes: 294 additions & 1 deletion src/devrev_mcp/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
import asyncio
import os
import requests
import json
import httpx

from mcp.server.models import InitializationOptions
import mcp.types as types
Expand All @@ -30,6 +32,40 @@ async def handle_list_tools() -> list[types.Tool]:
description="Fetch the current DevRev user details. When the user specifies 'me' in the query, this tool should be called to get the user details.",
inputSchema={"type": "object", "properties": {}},
),
types.Tool(
name="get_vistas",
description="Retrieve all available vistas (filtered views) from DevRev",
inputSchema={
"type": "object",
"properties": {
"id": {
"type": "string",
"description": "The DevRev ID of the vista"
}
},
"required": ["id"]
},
),
types.Tool(
name="list_vistas",
description="List all available vistas (filtered views) from DevRev with optional filtering",
inputSchema={
"type": "object",
"properties": {
"type": {"type": "array", "items": {"type": "string", "enum": ["curated", "dynamic", "grouped"]}, "description": "Filters for vistas of the specific type"},
"cursor": {"type": "string", "description": "The cursor to resume iteration from. If not provided, iteration begins from the first page"},
"created_by": {"type": "array", "items": {"type": "string"}, "description": "Filters for vistas created by any of these users"},
"flavor": {"type": "array", "items": {"type": "string", "enum": ["nnl", "sprint_board", "support_inbox"]}, "description": "Filters for vistas of specific flavor"},
"is_default": {"type": "boolean", "description": "Whether the default vistas should be fetched or not"},
"limit": {"type": "integer", "description": "The maximum number of vistas to return. The default is 50, the maximum is 100"},
"members": {"type": "array", "items": {"type": "string"}, "description": "Filters for vistas accessible to the input members"},
"mode": {"type": "string", "enum": ["after", "before"], "description": "The iteration mode to use. If 'after', then entries after the provided cursor will be returned. If 'before', then entries before the provided cursor will be returned"},
"sort_by": {"type": "array", "items": {"type": "string", "enum": ["target_start_date:asc", "target_start_date:desc", "target_close_date:asc", "target_close_date:desc", "actual_start_date:asc", "actual_start_date:desc", "actual_close_date:asc", "actual_close_date:desc", "created_date:asc", "created_date:desc"]}, "description": "The field (and the order) to sort the works by, in the sequence of the array elements"},
"state": {"type": "array", "items": {"type": "string", "enum": ["active", "completed", "planned"]}, "description": "Denotes the state of the vista group item"},
"skip_items": {"type": "boolean", "description": "Denotes whether to skip items of vista_group_item in response"}
}
},
),
types.Tool(
name="search",
description="Search DevRev using the provided query",
Expand All @@ -39,7 +75,7 @@ async def handle_list_tools() -> list[types.Tool]:
"query": {"type": "string"},
"namespace": {
"type": "string",
"enum": ["article", "issue", "ticket", "part", "dev_user", "account", "rev_org"],
"enum": ["article", "issue", "ticket", "part", "dev_user", "account", "rev_org", "vista"],
"description": "The namespace to search in. Use this to specify the type of object to search for."
},
},
Expand Down Expand Up @@ -408,6 +444,65 @@ async def handle_list_tools() -> list[types.Tool]:
"required": ["ancestor_part_id"],
},
),
types.Tool(
name="get_opportunity",
description="Get information about an opportunity using its ID",
inputSchema={
"type": "object",
"properties": {
"id": {"type": "string", "description": "The DevRev ID of the opportunity"},
},
"required": ["id"],
},
),
types.Tool(
name="list_opportunities",
description="List opportunities with filtering",
inputSchema={
"type": "object",
"properties": {
"cursor": {
"type": "object",
"properties": {
"next_cursor": {"type": "string", "description": "The cursor to use for pagination. If not provided, iteration begins from the first page."},
"mode": {"type": "string", "enum": ["after", "before"], "description": "The mode to iterate after the cursor or before the cursor ."},
},
"required": ["next_cursor", "mode"],
"description": "The cursor to use for pagination. If not provided, iteration begins from the first page. In the output you get next_cursor, use it and the correct mode to get the next or previous page. You can use these to loop through all the pages."
},
"owned_by": {"type": "array", "items": {"type": "string"}, "description": "The DevRev IDs of the users who own the opportunities "},
"account": {"type": "array", "items": {"type": "string"}, "description": "The account IDs associated with the opportunities "},
"modified_by": {"type": "array", "items": {"type": "string"}, "description": "The DevRev IDs of the users who modified the opportunities"},
"stage": {"type": "array", "items": {"type": "string"}, "description": "The stage names of the opportunities"},
"state": {"type": "array", "items": {"type": "string", "enum": ["open", "closed", "in_progress"]}, "description": "The state names of the opportunities "},
"sort_by": {"type": "array", "items": {"type": "string", "enum": ["created_date:asc", "created_date:desc", "modified_date:asc", "modified_date:desc", "target_close_date:asc", "target_close_date:desc"]}, "description": "The field (and the order) to sort the opportunities by, in the sequence of the array elements"},
"created_date": {
"type": "object",
"properties": {
"after": {"type": "string", "description": "The start date of the created date range, for example: 2025-06-03T00:00:00Z"},
"before": {"type": "string", "description": "The end date of the created date range, for example: 2025-06-03T00:00:00Z"},
},
"required": ["after", "before"]
},
"modified_date": {
"type": "object",
"properties": {
"after": {"type": "string", "description": "The start date of the modified date range, for example: 2025-06-03T00:00:00Z"},
"before": {"type": "string", "description": "The end date of the modified date range, for example: 2025-06-03T00:00:00Z"},
},
"required": ["after", "before"]
},
"target_close_date": {
"type": "object",
"properties": {
"after": {"type": "string", "description": "The start date of the target close date range, for example: 2025-06-03T00:00:00Z"},
"before": {"type": "string", "description": "The end date of the target close date range, for example: 2025-06-03T00:00:00Z"},
},
"required": ["after", "before"]
},
},
},
),
]

@server.call_tool()
Expand Down Expand Up @@ -439,6 +534,107 @@ async def handle_call_tool(
text=f"Current DevRev user details: {response.json()}"
)
]

elif name == "get_vistas":
if not arguments:
raise ValueError("Missing arguments")

id = arguments.get("id")
if not id:
raise ValueError("Missing id ")

response = make_devrev_request(
"vistas.get",
{
"id": id
}
)

if response.status_code != 200:
error_text = response.text
return [
types.TextContent(
type="text",
text=f"get_vistas failed with status {response.status_code}: {error_text}"
)
]

return [
types.TextContent(
type="text",
text=f"Vista details for '{id}':\n{response.json()}"
)
]

elif name == "list_vistas":
payload = {}

if arguments:
type_filter = arguments.get("type")
if type_filter:
payload["type"] = type_filter

cursor = arguments.get("cursor")
if cursor:
payload["cursor"] = cursor

created_by = arguments.get("created_by")
if created_by:
payload["created_by"] = created_by

flavor = arguments.get("flavor")
if flavor:
payload["flavor"] = flavor

is_default = arguments.get("is_default")
if is_default is not None:
payload["is_default"] = is_default

limit = arguments.get("limit")
if limit:
payload["limit"] = limit

members = arguments.get("members")
if members:
payload["members"] = members

mode = arguments.get("mode")
if mode:
payload["mode"] = mode

sort_by = arguments.get("sort_by")
if sort_by:
payload["sort_by"] = sort_by

state = arguments.get("state")
if state:
payload["state"] = state

skip_items = arguments.get("skip_items")
if skip_items is not None:
payload["skip_items"] = skip_items

response = make_devrev_request(
"vistas.list",
payload
)

if response.status_code != 200:
error_text = response.text
return [
types.TextContent(
type="text",
text=f"list_vistas failed with status {response.status_code}: {error_text}"
)
]

return [
types.TextContent(
type="text",
text=f"Vistas listed successfully:\n{response.json()}"
)
]

elif name == "search":
if not arguments:
raise ValueError("Missing arguments")
Expand Down Expand Up @@ -547,6 +743,7 @@ async def handle_call_tool(
text=f"Object created successfully: {response.json()}"
)
]

elif name == "update_work":
if not arguments:
raise ValueError("Missing arguments")
Expand Down Expand Up @@ -1210,6 +1407,102 @@ async def handle_call_tool(
text=f"Sprints for '{ancestor_part_id}':\n{sprints}"
)
]
elif name == "get_opportunity":
if not arguments:
raise ValueError("Missing arguments")

id = arguments.get("id")
if not id:
raise ValueError("Missing id parameter")

response = make_devrev_request(
"opportunities.get",
{
"id": id
}
)
if response.status_code != 200:
error_text = response.text
return [
types.TextContent(
type="text",
text=f"Get opportunity failed with status {response.status_code}: {error_text}"
)
]

return [
types.TextContent(
type="text",
text=f"Opportunity information for '{id}':\n{response.json()}"
)
]
elif name == "list_opportunities":
payload = {}

if arguments:
cursor = arguments.get("cursor")
if cursor:
payload["cursor"] = cursor["next_cursor"]
payload["mode"] = cursor["mode"]

owned_by = arguments.get("owned_by")
if owned_by:
payload["owned_by"] = owned_by

account = arguments.get("account")
if account:
payload["account"] = account



modified_by = arguments.get("modified_by")
if modified_by:
payload["modified_by"] = modified_by

stage = arguments.get("stage")
if stage:
payload["stage"] = stage

state = arguments.get("state")
if state:
payload["state"] = state

sort_by = arguments.get("sort_by")
if sort_by:
payload["sort_by"] = sort_by

created_date = arguments.get("created_date")
if created_date:
payload["created_date"] = {"type": "range", "after": created_date["after"], "before": created_date["before"]}

modified_date = arguments.get("modified_date")
if modified_date:
payload["modified_date"] = {"type": "range", "after": modified_date["after"], "before": modified_date["before"]}

target_close_date = arguments.get("target_close_date")
if target_close_date:
payload["target_close_date"] = {"type": "range", "after": target_close_date["after"], "before": target_close_date["before"]}

response = make_devrev_request(
"opportunities.list",
payload
)

if response.status_code != 200:
error_text = response.text
return [
types.TextContent(
type="text",
text=f"List opportunities failed with status {response.status_code}: {error_text}"
)
]

return [
types.TextContent(
type="text",
text=f"Opportunities listed successfully:\n{response.json()}"
)
]
else:
raise ValueError(f"Unknown tool: {name}")

Expand Down
Loading