feat: initial commit from workspace-mcp
Some checks failed
Check Maintainer Edits Enabled / check-maintainer-edits (pull_request) Has been cancelled
Check Maintainer Edits Enabled / check-maintainer-edits-internal (pull_request) Has been cancelled
Docker Build and Push to GHCR / build-and-push (pull_request) Has been cancelled
Ruff / ruff (pull_request) Has been cancelled
Some checks failed
Check Maintainer Edits Enabled / check-maintainer-edits (pull_request) Has been cancelled
Check Maintainer Edits Enabled / check-maintainer-edits-internal (pull_request) Has been cancelled
Docker Build and Push to GHCR / build-and-push (pull_request) Has been cancelled
Ruff / ruff (pull_request) Has been cancelled
This commit is contained in:
3
gforms/__init__.py
Normal file
3
gforms/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
"""
|
||||
Google Forms MCP Tools module
|
||||
"""
|
||||
487
gforms/forms_tools.py
Normal file
487
gforms/forms_tools.py
Normal file
@@ -0,0 +1,487 @@
|
||||
"""
|
||||
Google Forms MCP Tools
|
||||
|
||||
This module provides MCP tools for interacting with Google Forms API.
|
||||
"""
|
||||
|
||||
import logging
|
||||
import asyncio
|
||||
import json
|
||||
from typing import List, Optional, Dict, Any
|
||||
|
||||
|
||||
from auth.service_decorator import require_google_service
|
||||
from core.server import server
|
||||
from core.utils import handle_http_errors
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _extract_option_values(options: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
||||
"""Extract valid option objects from Forms choice option objects.
|
||||
|
||||
Returns the full option dicts (preserving fields like ``isOther``,
|
||||
``image``, ``goToAction``, and ``goToSectionId``) while filtering
|
||||
out entries that lack a truthy ``value``.
|
||||
"""
|
||||
return [option for option in options if option.get("value")]
|
||||
|
||||
|
||||
def _get_question_type(question: Dict[str, Any]) -> str:
|
||||
"""Infer a stable question/item type label from a Forms question payload."""
|
||||
choice_question = question.get("choiceQuestion")
|
||||
if choice_question:
|
||||
return choice_question.get("type", "CHOICE")
|
||||
|
||||
text_question = question.get("textQuestion")
|
||||
if text_question:
|
||||
return "PARAGRAPH" if text_question.get("paragraph") else "TEXT"
|
||||
|
||||
if "rowQuestion" in question:
|
||||
return "GRID_ROW"
|
||||
if "scaleQuestion" in question:
|
||||
return "SCALE"
|
||||
if "dateQuestion" in question:
|
||||
return "DATE"
|
||||
if "timeQuestion" in question:
|
||||
return "TIME"
|
||||
if "fileUploadQuestion" in question:
|
||||
return "FILE_UPLOAD"
|
||||
if "ratingQuestion" in question:
|
||||
return "RATING"
|
||||
|
||||
return "QUESTION"
|
||||
|
||||
|
||||
def _serialize_form_item(item: Dict[str, Any], index: int) -> Dict[str, Any]:
|
||||
"""Serialize a Forms item with the key metadata agents need for edits."""
|
||||
serialized_item: Dict[str, Any] = {
|
||||
"index": index,
|
||||
"itemId": item.get("itemId"),
|
||||
"title": item.get("title", f"Question {index}"),
|
||||
}
|
||||
|
||||
if item.get("description"):
|
||||
serialized_item["description"] = item["description"]
|
||||
|
||||
if "questionItem" in item:
|
||||
question = item.get("questionItem", {}).get("question", {})
|
||||
serialized_item["type"] = _get_question_type(question)
|
||||
serialized_item["required"] = question.get("required", False)
|
||||
|
||||
question_id = question.get("questionId")
|
||||
if question_id:
|
||||
serialized_item["questionId"] = question_id
|
||||
|
||||
choice_question = question.get("choiceQuestion")
|
||||
if choice_question:
|
||||
serialized_item["options"] = _extract_option_values(
|
||||
choice_question.get("options", [])
|
||||
)
|
||||
|
||||
return serialized_item
|
||||
|
||||
if "questionGroupItem" in item:
|
||||
question_group = item.get("questionGroupItem", {})
|
||||
columns = _extract_option_values(
|
||||
question_group.get("grid", {}).get("columns", {}).get("options", [])
|
||||
)
|
||||
|
||||
rows = []
|
||||
for question in question_group.get("questions", []):
|
||||
row: Dict[str, Any] = {
|
||||
"title": question.get("rowQuestion", {}).get("title", "")
|
||||
}
|
||||
row_question_id = question.get("questionId")
|
||||
if row_question_id:
|
||||
row["questionId"] = row_question_id
|
||||
row["required"] = question.get("required", False)
|
||||
rows.append(row)
|
||||
|
||||
serialized_item["type"] = "GRID"
|
||||
serialized_item["grid"] = {"rows": rows, "columns": columns}
|
||||
return serialized_item
|
||||
|
||||
if "pageBreakItem" in item:
|
||||
serialized_item["type"] = "PAGE_BREAK"
|
||||
elif "textItem" in item:
|
||||
serialized_item["type"] = "TEXT_ITEM"
|
||||
elif "imageItem" in item:
|
||||
serialized_item["type"] = "IMAGE"
|
||||
elif "videoItem" in item:
|
||||
serialized_item["type"] = "VIDEO"
|
||||
else:
|
||||
serialized_item["type"] = "UNKNOWN"
|
||||
|
||||
return serialized_item
|
||||
|
||||
|
||||
@server.tool()
|
||||
@handle_http_errors("create_form", service_type="forms")
|
||||
@require_google_service("forms", "forms")
|
||||
async def create_form(
|
||||
service,
|
||||
user_google_email: str,
|
||||
title: str,
|
||||
description: Optional[str] = None,
|
||||
document_title: Optional[str] = None,
|
||||
) -> str:
|
||||
"""
|
||||
Create a new form using the title given in the provided form message in the request.
|
||||
|
||||
Args:
|
||||
user_google_email (str): The user's Google email address. Required.
|
||||
title (str): The title of the form.
|
||||
description (Optional[str]): The description of the form.
|
||||
document_title (Optional[str]): The document title (shown in browser tab).
|
||||
|
||||
Returns:
|
||||
str: Confirmation message with form ID and edit URL.
|
||||
"""
|
||||
logger.info(f"[create_form] Invoked. Email: '{user_google_email}', Title: {title}")
|
||||
|
||||
form_body: Dict[str, Any] = {"info": {"title": title}}
|
||||
|
||||
if description:
|
||||
form_body["info"]["description"] = description
|
||||
|
||||
if document_title:
|
||||
form_body["info"]["document_title"] = document_title
|
||||
|
||||
created_form = await asyncio.to_thread(
|
||||
service.forms().create(body=form_body).execute
|
||||
)
|
||||
|
||||
form_id = created_form.get("formId")
|
||||
edit_url = f"https://docs.google.com/forms/d/{form_id}/edit"
|
||||
responder_url = created_form.get(
|
||||
"responderUri", f"https://docs.google.com/forms/d/{form_id}/viewform"
|
||||
)
|
||||
|
||||
confirmation_message = f"Successfully created form '{created_form.get('info', {}).get('title', title)}' for {user_google_email}. Form ID: {form_id}. Edit URL: {edit_url}. Responder URL: {responder_url}"
|
||||
logger.info(f"Form created successfully for {user_google_email}. ID: {form_id}")
|
||||
return confirmation_message
|
||||
|
||||
|
||||
@server.tool()
|
||||
@handle_http_errors("get_form", is_read_only=True, service_type="forms")
|
||||
@require_google_service("forms", "forms")
|
||||
async def get_form(service, user_google_email: str, form_id: str) -> str:
|
||||
"""
|
||||
Get a form.
|
||||
|
||||
Args:
|
||||
user_google_email (str): The user's Google email address. Required.
|
||||
form_id (str): The ID of the form to retrieve.
|
||||
|
||||
Returns:
|
||||
str: Form details including title, description, questions, and URLs.
|
||||
"""
|
||||
logger.info(f"[get_form] Invoked. Email: '{user_google_email}', Form ID: {form_id}")
|
||||
|
||||
form = await asyncio.to_thread(service.forms().get(formId=form_id).execute)
|
||||
|
||||
form_info = form.get("info", {})
|
||||
title = form_info.get("title", "No Title")
|
||||
description = form_info.get("description", "No Description")
|
||||
document_title = form_info.get("documentTitle", title)
|
||||
|
||||
edit_url = f"https://docs.google.com/forms/d/{form_id}/edit"
|
||||
responder_url = form.get(
|
||||
"responderUri", f"https://docs.google.com/forms/d/{form_id}/viewform"
|
||||
)
|
||||
|
||||
items = form.get("items", [])
|
||||
serialized_items = [
|
||||
_serialize_form_item(item, i) for i, item in enumerate(items, 1)
|
||||
]
|
||||
|
||||
items_summary = []
|
||||
for serialized_item in serialized_items:
|
||||
item_index = serialized_item["index"]
|
||||
item_title = serialized_item.get("title", f"Item {item_index}")
|
||||
item_type = serialized_item.get("type", "UNKNOWN")
|
||||
required_text = " (Required)" if serialized_item.get("required") else ""
|
||||
items_summary.append(
|
||||
f" {item_index}. {item_title} [{item_type}]{required_text}"
|
||||
)
|
||||
|
||||
items_summary_text = (
|
||||
"\n".join(items_summary) if items_summary else " No items found"
|
||||
)
|
||||
items_text = json.dumps(serialized_items, indent=2) if serialized_items else "[]"
|
||||
|
||||
result = f"""Form Details for {user_google_email}:
|
||||
- Title: "{title}"
|
||||
- Description: "{description}"
|
||||
- Document Title: "{document_title}"
|
||||
- Form ID: {form_id}
|
||||
- Edit URL: {edit_url}
|
||||
- Responder URL: {responder_url}
|
||||
- Items ({len(items)} total):
|
||||
{items_summary_text}
|
||||
- Items (structured):
|
||||
{items_text}"""
|
||||
|
||||
logger.info(f"Successfully retrieved form for {user_google_email}. ID: {form_id}")
|
||||
return result
|
||||
|
||||
|
||||
@server.tool()
|
||||
@handle_http_errors("set_publish_settings", service_type="forms")
|
||||
@require_google_service("forms", "forms")
|
||||
async def set_publish_settings(
|
||||
service,
|
||||
user_google_email: str,
|
||||
form_id: str,
|
||||
publish_as_template: bool = False,
|
||||
require_authentication: bool = False,
|
||||
) -> str:
|
||||
"""
|
||||
Updates the publish settings of a form.
|
||||
|
||||
Args:
|
||||
user_google_email (str): The user's Google email address. Required.
|
||||
form_id (str): The ID of the form to update publish settings for.
|
||||
publish_as_template (bool): Whether to publish as a template. Defaults to False.
|
||||
require_authentication (bool): Whether to require authentication to view/submit. Defaults to False.
|
||||
|
||||
Returns:
|
||||
str: Confirmation message of the successful publish settings update.
|
||||
"""
|
||||
logger.info(
|
||||
f"[set_publish_settings] Invoked. Email: '{user_google_email}', Form ID: {form_id}"
|
||||
)
|
||||
|
||||
settings_body = {
|
||||
"publishAsTemplate": publish_as_template,
|
||||
"requireAuthentication": require_authentication,
|
||||
}
|
||||
|
||||
await asyncio.to_thread(
|
||||
service.forms().setPublishSettings(formId=form_id, body=settings_body).execute
|
||||
)
|
||||
|
||||
confirmation_message = f"Successfully updated publish settings for form {form_id} for {user_google_email}. Publish as template: {publish_as_template}, Require authentication: {require_authentication}"
|
||||
logger.info(
|
||||
f"Publish settings updated successfully for {user_google_email}. Form ID: {form_id}"
|
||||
)
|
||||
return confirmation_message
|
||||
|
||||
|
||||
@server.tool()
|
||||
@handle_http_errors("get_form_response", is_read_only=True, service_type="forms")
|
||||
@require_google_service("forms", "forms")
|
||||
async def get_form_response(
|
||||
service, user_google_email: str, form_id: str, response_id: str
|
||||
) -> str:
|
||||
"""
|
||||
Get one response from the form.
|
||||
|
||||
Args:
|
||||
user_google_email (str): The user's Google email address. Required.
|
||||
form_id (str): The ID of the form.
|
||||
response_id (str): The ID of the response to retrieve.
|
||||
|
||||
Returns:
|
||||
str: Response details including answers and metadata.
|
||||
"""
|
||||
logger.info(
|
||||
f"[get_form_response] Invoked. Email: '{user_google_email}', Form ID: {form_id}, Response ID: {response_id}"
|
||||
)
|
||||
|
||||
response = await asyncio.to_thread(
|
||||
service.forms().responses().get(formId=form_id, responseId=response_id).execute
|
||||
)
|
||||
|
||||
response_id = response.get("responseId", "Unknown")
|
||||
create_time = response.get("createTime", "Unknown")
|
||||
last_submitted_time = response.get("lastSubmittedTime", "Unknown")
|
||||
|
||||
answers = response.get("answers", {})
|
||||
answer_details = []
|
||||
for question_id, answer_data in answers.items():
|
||||
question_response = answer_data.get("textAnswers", {}).get("answers", [])
|
||||
if question_response:
|
||||
answer_text = ", ".join([ans.get("value", "") for ans in question_response])
|
||||
answer_details.append(f" Question ID {question_id}: {answer_text}")
|
||||
else:
|
||||
answer_details.append(f" Question ID {question_id}: No answer provided")
|
||||
|
||||
answers_text = "\n".join(answer_details) if answer_details else " No answers found"
|
||||
|
||||
result = f"""Form Response Details for {user_google_email}:
|
||||
- Form ID: {form_id}
|
||||
- Response ID: {response_id}
|
||||
- Created: {create_time}
|
||||
- Last Submitted: {last_submitted_time}
|
||||
- Answers:
|
||||
{answers_text}"""
|
||||
|
||||
logger.info(
|
||||
f"Successfully retrieved response for {user_google_email}. Response ID: {response_id}"
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
@server.tool()
|
||||
@handle_http_errors("list_form_responses", is_read_only=True, service_type="forms")
|
||||
@require_google_service("forms", "forms")
|
||||
async def list_form_responses(
|
||||
service,
|
||||
user_google_email: str,
|
||||
form_id: str,
|
||||
page_size: int = 10,
|
||||
page_token: Optional[str] = None,
|
||||
) -> str:
|
||||
"""
|
||||
List a form's responses.
|
||||
|
||||
Args:
|
||||
user_google_email (str): The user's Google email address. Required.
|
||||
form_id (str): The ID of the form.
|
||||
page_size (int): Maximum number of responses to return. Defaults to 10.
|
||||
page_token (Optional[str]): Token for retrieving next page of results.
|
||||
|
||||
Returns:
|
||||
str: List of responses with basic details and pagination info.
|
||||
"""
|
||||
logger.info(
|
||||
f"[list_form_responses] Invoked. Email: '{user_google_email}', Form ID: {form_id}"
|
||||
)
|
||||
|
||||
params = {"formId": form_id, "pageSize": page_size}
|
||||
if page_token:
|
||||
params["pageToken"] = page_token
|
||||
|
||||
responses_result = await asyncio.to_thread(
|
||||
service.forms().responses().list(**params).execute
|
||||
)
|
||||
|
||||
responses = responses_result.get("responses", [])
|
||||
next_page_token = responses_result.get("nextPageToken")
|
||||
|
||||
if not responses:
|
||||
return f"No responses found for form {form_id} for {user_google_email}."
|
||||
|
||||
response_details = []
|
||||
for i, response in enumerate(responses, 1):
|
||||
response_id = response.get("responseId", "Unknown")
|
||||
create_time = response.get("createTime", "Unknown")
|
||||
last_submitted_time = response.get("lastSubmittedTime", "Unknown")
|
||||
|
||||
answers_count = len(response.get("answers", {}))
|
||||
response_details.append(
|
||||
f" {i}. Response ID: {response_id} | Created: {create_time} | Last Submitted: {last_submitted_time} | Answers: {answers_count}"
|
||||
)
|
||||
|
||||
pagination_info = (
|
||||
f"\nNext page token: {next_page_token}"
|
||||
if next_page_token
|
||||
else "\nNo more pages."
|
||||
)
|
||||
|
||||
result = f"""Form Responses for {user_google_email}:
|
||||
- Form ID: {form_id}
|
||||
- Total responses returned: {len(responses)}
|
||||
- Responses:
|
||||
{chr(10).join(response_details)}{pagination_info}"""
|
||||
|
||||
logger.info(
|
||||
f"Successfully retrieved {len(responses)} responses for {user_google_email}. Form ID: {form_id}"
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
# Internal implementation function for testing
|
||||
async def _batch_update_form_impl(
|
||||
service: Any,
|
||||
form_id: str,
|
||||
requests: List[Dict[str, Any]],
|
||||
) -> str:
|
||||
"""Internal implementation for batch_update_form.
|
||||
|
||||
Applies batch updates to a Google Form using the Forms API batchUpdate method.
|
||||
|
||||
Args:
|
||||
service: Google Forms API service client.
|
||||
form_id: The ID of the form to update.
|
||||
requests: List of update request dictionaries.
|
||||
|
||||
Returns:
|
||||
Formatted string with batch update results.
|
||||
"""
|
||||
body = {"requests": requests}
|
||||
|
||||
result = await asyncio.to_thread(
|
||||
service.forms().batchUpdate(formId=form_id, body=body).execute
|
||||
)
|
||||
|
||||
replies = result.get("replies", [])
|
||||
|
||||
confirmation_message = f"""Batch Update Completed:
|
||||
- Form ID: {form_id}
|
||||
- URL: https://docs.google.com/forms/d/{form_id}/edit
|
||||
- Requests Applied: {len(requests)}
|
||||
- Replies Received: {len(replies)}"""
|
||||
|
||||
if replies:
|
||||
confirmation_message += "\n\nUpdate Results:"
|
||||
for i, reply in enumerate(replies, 1):
|
||||
if "createItem" in reply:
|
||||
item_id = reply["createItem"].get("itemId", "Unknown")
|
||||
question_ids = reply["createItem"].get("questionId", [])
|
||||
question_info = (
|
||||
f" (Question IDs: {', '.join(question_ids)})"
|
||||
if question_ids
|
||||
else ""
|
||||
)
|
||||
confirmation_message += (
|
||||
f"\n Request {i}: Created item {item_id}{question_info}"
|
||||
)
|
||||
else:
|
||||
confirmation_message += f"\n Request {i}: Operation completed"
|
||||
|
||||
return confirmation_message
|
||||
|
||||
|
||||
@server.tool()
|
||||
@handle_http_errors("batch_update_form", service_type="forms")
|
||||
@require_google_service("forms", "forms")
|
||||
async def batch_update_form(
|
||||
service,
|
||||
user_google_email: str,
|
||||
form_id: str,
|
||||
requests: List[Dict[str, Any]],
|
||||
) -> str:
|
||||
"""
|
||||
Apply batch updates to a Google Form.
|
||||
|
||||
Supports adding, updating, and deleting form items, as well as updating
|
||||
form metadata and settings. This is the primary method for modifying form
|
||||
content after creation.
|
||||
|
||||
Args:
|
||||
user_google_email (str): The user's Google email address. Required.
|
||||
form_id (str): The ID of the form to update.
|
||||
requests (List[Dict[str, Any]]): List of update requests to apply.
|
||||
Supported request types:
|
||||
- createItem: Add a new question or content item
|
||||
- updateItem: Modify an existing item
|
||||
- deleteItem: Remove an item
|
||||
- moveItem: Reorder an item
|
||||
- updateFormInfo: Update form title/description
|
||||
- updateSettings: Modify form settings (e.g., quiz mode)
|
||||
|
||||
Returns:
|
||||
str: Details about the batch update operation results.
|
||||
"""
|
||||
logger.info(
|
||||
f"[batch_update_form] Invoked. Email: '{user_google_email}', "
|
||||
f"Form ID: '{form_id}', Requests: {len(requests)}"
|
||||
)
|
||||
|
||||
result = await _batch_update_form_impl(service, form_id, requests)
|
||||
|
||||
logger.info(f"Batch update completed successfully for {user_google_email}")
|
||||
return result
|
||||
Reference in New Issue
Block a user