added feature google docs integration
This commit is contained in:
@@ -50,4 +50,44 @@ INTEGRATION_CREDENTIALS = {
|
||||
credential_id="hubspot",
|
||||
credential_key="access_token",
|
||||
),
|
||||
"google_docs": CredentialSpec(
|
||||
env_var="GOOGLE_DOCS_ACCESS_TOKEN",
|
||||
tools=[
|
||||
"google_docs_create_document",
|
||||
"google_docs_get_document",
|
||||
"google_docs_insert_text",
|
||||
"google_docs_replace_all_text",
|
||||
"google_docs_insert_image",
|
||||
"google_docs_format_text",
|
||||
"google_docs_batch_update",
|
||||
"google_docs_create_list",
|
||||
"google_docs_add_comment",
|
||||
"google_docs_export_content",
|
||||
],
|
||||
required=True,
|
||||
startup_required=False,
|
||||
help_url="https://console.cloud.google.com/apis/credentials",
|
||||
description="Google Docs OAuth2 access token",
|
||||
# Auth method support
|
||||
aden_supported=True,
|
||||
aden_provider_name="google",
|
||||
direct_api_key_supported=True,
|
||||
api_key_instructions="""To get a Google Docs access token:
|
||||
1. Go to Google Cloud Console: https://console.cloud.google.com/
|
||||
2. Create a new project or select an existing one
|
||||
3. Enable the Google Docs API and Google Drive API
|
||||
4. Go to APIs & Services > Credentials
|
||||
5. Create OAuth 2.0 credentials (Web application or Desktop app)
|
||||
6. Use the OAuth 2.0 Playground or your app to get an access token
|
||||
7. Required scopes:
|
||||
- https://www.googleapis.com/auth/documents
|
||||
- https://www.googleapis.com/auth/drive.file
|
||||
- https://www.googleapis.com/auth/drive (for export/comments)""",
|
||||
# Health check configuration
|
||||
health_check_endpoint="https://docs.googleapis.com/v1/documents/1",
|
||||
health_check_method="GET",
|
||||
# Credential store mapping
|
||||
credential_id="google_docs",
|
||||
credential_key="access_token",
|
||||
),
|
||||
}
|
||||
|
||||
@@ -38,6 +38,7 @@ from .file_system_toolkits.replace_file_content import (
|
||||
# Import file system toolkits
|
||||
from .file_system_toolkits.view_file import register_tools as register_view_file
|
||||
from .file_system_toolkits.write_to_file import register_tools as register_write_to_file
|
||||
from .google_docs_tool import register_tools as register_google_docs
|
||||
from .hubspot_tool import register_tools as register_hubspot
|
||||
from .pdf_read_tool import register_tools as register_pdf_read
|
||||
from .web_scrape_tool import register_tools as register_web_scrape
|
||||
@@ -70,6 +71,8 @@ def register_all_tools(
|
||||
# email supports multiple providers (Resend) with auto-detection
|
||||
register_email(mcp, credentials=credentials)
|
||||
register_hubspot(mcp, credentials=credentials)
|
||||
# Google Docs integration
|
||||
register_google_docs(mcp, credentials=credentials)
|
||||
|
||||
# Register file system toolkits
|
||||
register_view_file(mcp)
|
||||
@@ -114,6 +117,16 @@ def register_all_tools(
|
||||
"hubspot_get_deal",
|
||||
"hubspot_create_deal",
|
||||
"hubspot_update_deal",
|
||||
"google_docs_create_document",
|
||||
"google_docs_get_document",
|
||||
"google_docs_insert_text",
|
||||
"google_docs_replace_all_text",
|
||||
"google_docs_insert_image",
|
||||
"google_docs_format_text",
|
||||
"google_docs_batch_update",
|
||||
"google_docs_create_list",
|
||||
"google_docs_add_comment",
|
||||
"google_docs_export_content",
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,160 @@
|
||||
# Google Docs Tool
|
||||
|
||||
Create and manage Google Docs documents via the Google Docs API v1.
|
||||
|
||||
## Features
|
||||
|
||||
- Create new documents
|
||||
- Read document content and structure
|
||||
- Insert text at specific positions
|
||||
- Find and replace text (template population)
|
||||
- Insert images
|
||||
- Format text (bold, italic, colors, etc.)
|
||||
- Create bulleted and numbered lists
|
||||
- Add comments
|
||||
- Export to PDF, DOCX, TXT, and more
|
||||
|
||||
## Setup
|
||||
|
||||
### Option 1: OAuth2 Access Token (Recommended for Development)
|
||||
|
||||
1. Go to [Google Cloud Console](https://console.cloud.google.com/)
|
||||
2. Create a new project or select existing
|
||||
3. Enable the **Google Docs API** and **Google Drive API**
|
||||
4. Create OAuth 2.0 credentials
|
||||
5. Use the OAuth2 Playground or your app to get an access token
|
||||
6. Set the environment variable:
|
||||
|
||||
```bash
|
||||
export GOOGLE_DOCS_ACCESS_TOKEN="your-access-token"
|
||||
```
|
||||
|
||||
### Option 2: Service Account (Recommended for Production)
|
||||
|
||||
1. Create a service account in Google Cloud Console
|
||||
2. Download the JSON key file
|
||||
3. Share your documents with the service account email
|
||||
4. Set the environment variable:
|
||||
|
||||
```bash
|
||||
export GOOGLE_SERVICE_ACCOUNT_JSON='{"type":"service_account",...}'
|
||||
```
|
||||
|
||||
### Required OAuth Scopes
|
||||
|
||||
- `https://www.googleapis.com/auth/documents` - Full access to Google Docs
|
||||
- `https://www.googleapis.com/auth/drive.file` - Access to files created/opened by the app
|
||||
- `https://www.googleapis.com/auth/drive` - For export and comments functionality
|
||||
|
||||
## Available Tools
|
||||
|
||||
| Tool | Description |
|
||||
|------|-------------|
|
||||
| `google_docs_create_document` | Create a new blank document with a specified title |
|
||||
| `google_docs_get_document` | Retrieve the full structural content of a document |
|
||||
| `google_docs_insert_text` | Insert text at a specific index or at the end |
|
||||
| `google_docs_replace_all_text` | Global find-and-replace for template population |
|
||||
| `google_docs_insert_image` | Insert images via public URI |
|
||||
| `google_docs_format_text` | Apply styling (bold, italic, colors, font size) |
|
||||
| `google_docs_batch_update` | Execute multiple requests atomically |
|
||||
| `google_docs_create_list` | Create bulleted or numbered lists |
|
||||
| `google_docs_add_comment` | Add comments to documents |
|
||||
| `google_docs_export_content` | Export to PDF, DOCX, TXT, HTML, etc. |
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Create a Document
|
||||
|
||||
```python
|
||||
result = google_docs_create_document(title="My New Document")
|
||||
# Returns: {"document_id": "1abc...", "title": "My New Document", "document_url": "https://docs.google.com/..."}
|
||||
```
|
||||
|
||||
### Populate a Template
|
||||
|
||||
```python
|
||||
# Use placeholders in your template like {{Customer_Name}}, {{Date}}, etc.
|
||||
result = google_docs_replace_all_text(
|
||||
document_id="1abc...",
|
||||
find_text="{{Customer_Name}}",
|
||||
replace_text="John Doe"
|
||||
)
|
||||
# Returns: {"occurrences_replaced": 3}
|
||||
```
|
||||
|
||||
### Insert Text
|
||||
|
||||
```python
|
||||
# Insert at the end
|
||||
result = google_docs_insert_text(
|
||||
document_id="1abc...",
|
||||
text="Hello, World!\n"
|
||||
)
|
||||
|
||||
# Insert at specific position (1-based index)
|
||||
result = google_docs_insert_text(
|
||||
document_id="1abc...",
|
||||
text="Inserted text",
|
||||
index=10
|
||||
)
|
||||
```
|
||||
|
||||
### Format Text
|
||||
|
||||
```python
|
||||
result = google_docs_format_text(
|
||||
document_id="1abc...",
|
||||
start_index=1,
|
||||
end_index=12,
|
||||
bold=True,
|
||||
font_size_pt=18.0,
|
||||
foreground_color_red=0.0,
|
||||
foreground_color_green=0.0,
|
||||
foreground_color_blue=1.0 # Blue text
|
||||
)
|
||||
```
|
||||
|
||||
### Export to PDF
|
||||
|
||||
```python
|
||||
result = google_docs_export_content(
|
||||
document_id="1abc...",
|
||||
format="pdf"
|
||||
)
|
||||
# Returns: {"content_base64": "...", "size_bytes": 12345, "mime_type": "application/pdf"}
|
||||
```
|
||||
|
||||
## Technical Notes
|
||||
|
||||
### Document Indexing
|
||||
|
||||
The Google Docs API uses **1-based indexing** for document positions:
|
||||
- Index 1 is the start of the document body
|
||||
- For complex updates, it's recommended to **write backwards** (start from the end) to avoid index shifting
|
||||
|
||||
### Comments API
|
||||
|
||||
Adding and listing comments uses the Google Drive API (`drive.googleapis.com/v3/files/{fileId}/comments`), not the Docs API directly.
|
||||
|
||||
### Image Insertion
|
||||
|
||||
The `insertInlineImage` request requires a **publicly accessible URL**. Google's servers must be able to fetch the image from this URL.
|
||||
|
||||
## Error Handling
|
||||
|
||||
All tools return a dict. On error, the dict contains an `"error"` key with a description:
|
||||
|
||||
```python
|
||||
{"error": "Document not found"}
|
||||
{"error": "Invalid or expired Google access token"}
|
||||
{"error": "Insufficient permissions. Check your Google API scopes."}
|
||||
```
|
||||
|
||||
## Environment Variables
|
||||
|
||||
| Variable | Required | Description |
|
||||
|----------|----------|-------------|
|
||||
| `GOOGLE_DOCS_ACCESS_TOKEN` | Yes* | OAuth2 access token |
|
||||
| `GOOGLE_SERVICE_ACCOUNT_JSON` | Yes* | Service account JSON (alternative to access token) |
|
||||
|
||||
*One of these is required.
|
||||
@@ -0,0 +1,9 @@
|
||||
"""
|
||||
Google Docs Tool - Create and manage Google Docs documents.
|
||||
|
||||
Supports OAuth2 authentication via Google service account or OAuth2 tokens.
|
||||
"""
|
||||
|
||||
from .google_docs_tool import register_tools
|
||||
|
||||
__all__ = ["register_tools"]
|
||||
@@ -0,0 +1,688 @@
|
||||
"""
|
||||
Google Docs Tool - Create and manage Google Docs documents via Google Docs API v1.
|
||||
|
||||
Supports:
|
||||
- OAuth2 tokens via the credential store
|
||||
- Service account JSON credentials (GOOGLE_SERVICE_ACCOUNT_JSON)
|
||||
- Direct access token (GOOGLE_DOCS_ACCESS_TOKEN)
|
||||
|
||||
API Reference: https://developers.google.com/docs/api/reference/rest
|
||||
|
||||
Note on indexing: The Google Docs API uses 1-based indexing for document content.
|
||||
For complex updates, it's recommended to "write backwards" (start from the end
|
||||
of the document) to avoid index shifting issues.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
import httpx
|
||||
from fastmcp import FastMCP
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from aden_tools.credentials import CredentialStoreAdapter
|
||||
|
||||
GOOGLE_DOCS_API_BASE = "https://docs.googleapis.com/v1"
|
||||
GOOGLE_DRIVE_API_BASE = "https://www.googleapis.com/drive/v3"
|
||||
|
||||
|
||||
class _GoogleDocsClient:
|
||||
"""Internal client wrapping Google Docs API v1 calls."""
|
||||
|
||||
def __init__(self, access_token: str):
|
||||
self._token = access_token
|
||||
|
||||
@property
|
||||
def _headers(self) -> dict[str, str]:
|
||||
return {
|
||||
"Authorization": f"Bearer {self._token}",
|
||||
"Content-Type": "application/json",
|
||||
"Accept": "application/json",
|
||||
}
|
||||
|
||||
def _handle_response(self, response: httpx.Response) -> dict[str, Any]:
|
||||
"""Handle common HTTP error codes."""
|
||||
if response.status_code == 401:
|
||||
return {"error": "Invalid or expired Google access token"}
|
||||
if response.status_code == 403:
|
||||
return {
|
||||
"error": "Insufficient permissions. Check your Google API scopes. "
|
||||
"Required scopes: https://www.googleapis.com/auth/documents"
|
||||
}
|
||||
if response.status_code == 404:
|
||||
return {"error": "Document not found"}
|
||||
if response.status_code == 429:
|
||||
return {"error": "Google API rate limit exceeded. Try again later."}
|
||||
if response.status_code >= 400:
|
||||
try:
|
||||
error_data = response.json()
|
||||
detail = error_data.get("error", {}).get("message", response.text)
|
||||
except Exception:
|
||||
detail = response.text
|
||||
return {"error": f"Google Docs API error (HTTP {response.status_code}): {detail}"}
|
||||
return response.json()
|
||||
|
||||
def create_document(self, title: str) -> dict[str, Any]:
|
||||
"""Create a new blank document with a specified title."""
|
||||
response = httpx.post(
|
||||
f"{GOOGLE_DOCS_API_BASE}/documents",
|
||||
headers=self._headers,
|
||||
json={"title": title},
|
||||
timeout=30.0,
|
||||
)
|
||||
return self._handle_response(response)
|
||||
|
||||
def get_document(self, document_id: str) -> dict[str, Any]:
|
||||
"""Retrieve the full structural content, metadata, and elements of a document."""
|
||||
response = httpx.get(
|
||||
f"{GOOGLE_DOCS_API_BASE}/documents/{document_id}",
|
||||
headers=self._headers,
|
||||
timeout=30.0,
|
||||
)
|
||||
return self._handle_response(response)
|
||||
|
||||
def batch_update(
|
||||
self, document_id: str, requests: list[dict[str, Any]]
|
||||
) -> dict[str, Any]:
|
||||
"""Execute multiple requests in a single atomic operation."""
|
||||
response = httpx.post(
|
||||
f"{GOOGLE_DOCS_API_BASE}/documents/{document_id}:batchUpdate",
|
||||
headers=self._headers,
|
||||
json={"requests": requests},
|
||||
timeout=60.0,
|
||||
)
|
||||
return self._handle_response(response)
|
||||
|
||||
def insert_text(
|
||||
self,
|
||||
document_id: str,
|
||||
text: str,
|
||||
index: int | None = None,
|
||||
segment_id: str | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Insert text at a specific index or at the end of the document."""
|
||||
location: dict[str, Any] = {}
|
||||
if segment_id:
|
||||
location["segmentId"] = segment_id
|
||||
if index is not None:
|
||||
location["index"] = index
|
||||
else:
|
||||
# Insert at end - we need to get doc first to find the end index
|
||||
doc = self.get_document(document_id)
|
||||
if "error" in doc:
|
||||
return doc
|
||||
# Get the end index from the document body
|
||||
body = doc.get("body", {})
|
||||
content = body.get("content", [])
|
||||
if content:
|
||||
last_element = content[-1]
|
||||
end_index = last_element.get("endIndex", 1)
|
||||
location["index"] = end_index - 1 # Insert before the final newline
|
||||
else:
|
||||
location["index"] = 1
|
||||
|
||||
request = {
|
||||
"insertText": {
|
||||
"location": location,
|
||||
"text": text,
|
||||
}
|
||||
}
|
||||
return self.batch_update(document_id, [request])
|
||||
|
||||
def replace_all_text(
|
||||
self,
|
||||
document_id: str,
|
||||
find_text: str,
|
||||
replace_text: str,
|
||||
match_case: bool = True,
|
||||
) -> dict[str, Any]:
|
||||
"""Global find-and-replace (ideal for populating templates with dynamic data)."""
|
||||
request = {
|
||||
"replaceAllText": {
|
||||
"containsText": {
|
||||
"text": find_text,
|
||||
"matchCase": match_case,
|
||||
},
|
||||
"replaceText": replace_text,
|
||||
}
|
||||
}
|
||||
return self.batch_update(document_id, [request])
|
||||
|
||||
def insert_image(
|
||||
self,
|
||||
document_id: str,
|
||||
image_uri: str,
|
||||
index: int,
|
||||
width_pt: float | None = None,
|
||||
height_pt: float | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Insert an image into the document body via URI."""
|
||||
request: dict[str, Any] = {
|
||||
"insertInlineImage": {
|
||||
"location": {"index": index},
|
||||
"uri": image_uri,
|
||||
}
|
||||
}
|
||||
if width_pt is not None or height_pt is not None:
|
||||
object_size: dict[str, Any] = {}
|
||||
if width_pt is not None:
|
||||
object_size["width"] = {"magnitude": width_pt, "unit": "PT"}
|
||||
if height_pt is not None:
|
||||
object_size["height"] = {"magnitude": height_pt, "unit": "PT"}
|
||||
request["insertInlineImage"]["objectSize"] = object_size
|
||||
|
||||
return self.batch_update(document_id, [request])
|
||||
|
||||
def format_text(
|
||||
self,
|
||||
document_id: str,
|
||||
start_index: int,
|
||||
end_index: int,
|
||||
bold: bool | None = None,
|
||||
italic: bool | None = None,
|
||||
underline: bool | None = None,
|
||||
font_size_pt: float | None = None,
|
||||
foreground_color: dict[str, float] | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Apply styling (bold, italic, font size, colors) to specific text ranges."""
|
||||
text_style: dict[str, Any] = {}
|
||||
fields: list[str] = []
|
||||
|
||||
if bold is not None:
|
||||
text_style["bold"] = bold
|
||||
fields.append("bold")
|
||||
if italic is not None:
|
||||
text_style["italic"] = italic
|
||||
fields.append("italic")
|
||||
if underline is not None:
|
||||
text_style["underline"] = underline
|
||||
fields.append("underline")
|
||||
if font_size_pt is not None:
|
||||
text_style["fontSize"] = {"magnitude": font_size_pt, "unit": "PT"}
|
||||
fields.append("fontSize")
|
||||
if foreground_color is not None:
|
||||
text_style["foregroundColor"] = {"color": {"rgbColor": foreground_color}}
|
||||
fields.append("foregroundColor")
|
||||
|
||||
if not fields:
|
||||
return {"error": "No formatting options specified"}
|
||||
|
||||
request = {
|
||||
"updateTextStyle": {
|
||||
"range": {
|
||||
"startIndex": start_index,
|
||||
"endIndex": end_index,
|
||||
},
|
||||
"textStyle": text_style,
|
||||
"fields": ",".join(fields),
|
||||
}
|
||||
}
|
||||
return self.batch_update(document_id, [request])
|
||||
|
||||
def create_list(
|
||||
self,
|
||||
document_id: str,
|
||||
start_index: int,
|
||||
end_index: int,
|
||||
bullet_preset: str = "BULLET_DISC_CIRCLE_SQUARE",
|
||||
) -> dict[str, Any]:
|
||||
"""Create or modify bulleted and numbered lists within the document."""
|
||||
request = {
|
||||
"createParagraphBullets": {
|
||||
"range": {
|
||||
"startIndex": start_index,
|
||||
"endIndex": end_index,
|
||||
},
|
||||
"bulletPreset": bullet_preset,
|
||||
}
|
||||
}
|
||||
return self.batch_update(document_id, [request])
|
||||
|
||||
def add_comment(
|
||||
self,
|
||||
document_id: str,
|
||||
content: str,
|
||||
quoted_text: str | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Create a comment on the document (via Drive API)."""
|
||||
body: dict[str, Any] = {"content": content}
|
||||
if quoted_text:
|
||||
body["quotedFileContent"] = {"value": quoted_text}
|
||||
|
||||
response = httpx.post(
|
||||
f"{GOOGLE_DRIVE_API_BASE}/files/{document_id}/comments",
|
||||
headers=self._headers,
|
||||
params={"fields": "*"},
|
||||
json=body,
|
||||
timeout=30.0,
|
||||
)
|
||||
return self._handle_response(response)
|
||||
|
||||
def export_document(
|
||||
self,
|
||||
document_id: str,
|
||||
mime_type: str = "application/pdf",
|
||||
) -> dict[str, Any]:
|
||||
"""Export the document to different formats (PDF, DOCX, TXT)."""
|
||||
response = httpx.get(
|
||||
f"{GOOGLE_DRIVE_API_BASE}/files/{document_id}/export",
|
||||
headers=self._headers,
|
||||
params={"mimeType": mime_type},
|
||||
timeout=60.0,
|
||||
)
|
||||
if response.status_code == 200:
|
||||
# Return base64-encoded content for binary formats
|
||||
import base64
|
||||
|
||||
return {
|
||||
"document_id": document_id,
|
||||
"mime_type": mime_type,
|
||||
"content_base64": base64.b64encode(response.content).decode("utf-8"),
|
||||
"size_bytes": len(response.content),
|
||||
}
|
||||
return self._handle_response(response)
|
||||
|
||||
|
||||
def register_tools(
|
||||
mcp: FastMCP,
|
||||
credentials: CredentialStoreAdapter | None = None,
|
||||
) -> None:
|
||||
"""Register Google Docs tools with the MCP server."""
|
||||
|
||||
def _get_token() -> str | None:
|
||||
"""Get Google access token from credential manager or environment."""
|
||||
if credentials is not None:
|
||||
token = credentials.get("google_docs")
|
||||
if token is not None and not isinstance(token, str):
|
||||
raise TypeError(
|
||||
f"Expected string from credentials.get('google_docs'), "
|
||||
f"got {type(token).__name__}"
|
||||
)
|
||||
return token
|
||||
# Try environment variables
|
||||
token = os.getenv("GOOGLE_DOCS_ACCESS_TOKEN")
|
||||
if token:
|
||||
return token
|
||||
# Try service account JSON (would need additional handling for token exchange)
|
||||
service_account = os.getenv("GOOGLE_SERVICE_ACCOUNT_JSON")
|
||||
if service_account:
|
||||
# For service accounts, we'd need to implement JWT token exchange
|
||||
# This is a simplified version that expects a pre-exchanged token
|
||||
try:
|
||||
sa_data = json.loads(service_account)
|
||||
return sa_data.get("access_token")
|
||||
except json.JSONDecodeError:
|
||||
return None
|
||||
return None
|
||||
|
||||
def _get_client() -> _GoogleDocsClient | dict[str, str]:
|
||||
"""Get a Google Docs client, or return an error dict if no credentials."""
|
||||
token = _get_token()
|
||||
if not token:
|
||||
return {
|
||||
"error": "Google Docs credentials not configured",
|
||||
"help": (
|
||||
"Set GOOGLE_DOCS_ACCESS_TOKEN environment variable "
|
||||
"or configure via credential store. "
|
||||
"Get credentials at: https://console.cloud.google.com/apis/credentials"
|
||||
),
|
||||
}
|
||||
return _GoogleDocsClient(token)
|
||||
|
||||
# --- Document Management ---
|
||||
|
||||
@mcp.tool()
|
||||
def google_docs_create_document(title: str) -> dict:
|
||||
"""
|
||||
Create a new blank Google Docs document with a specified title.
|
||||
|
||||
Args:
|
||||
title: The title for the new document
|
||||
|
||||
Returns:
|
||||
Dict with document ID and metadata, or error
|
||||
"""
|
||||
client = _get_client()
|
||||
if isinstance(client, dict):
|
||||
return client
|
||||
try:
|
||||
result = client.create_document(title)
|
||||
if "error" not in result:
|
||||
return {
|
||||
"document_id": result.get("documentId"),
|
||||
"title": result.get("title"),
|
||||
"document_url": f"https://docs.google.com/document/d/{result.get('documentId')}/edit",
|
||||
}
|
||||
return result
|
||||
except httpx.TimeoutException:
|
||||
return {"error": "Request timed out"}
|
||||
except httpx.RequestError as e:
|
||||
return {"error": f"Network error: {e}"}
|
||||
|
||||
@mcp.tool()
|
||||
def google_docs_get_document(document_id: str) -> dict:
|
||||
"""
|
||||
Retrieve the full structural content, metadata, and elements of a document.
|
||||
|
||||
Args:
|
||||
document_id: The ID of the Google Docs document
|
||||
|
||||
Returns:
|
||||
Dict with document content and structure, or error
|
||||
"""
|
||||
client = _get_client()
|
||||
if isinstance(client, dict):
|
||||
return client
|
||||
try:
|
||||
return client.get_document(document_id)
|
||||
except httpx.TimeoutException:
|
||||
return {"error": "Request timed out"}
|
||||
except httpx.RequestError as e:
|
||||
return {"error": f"Network error: {e}"}
|
||||
|
||||
@mcp.tool()
|
||||
def google_docs_insert_text(
|
||||
document_id: str,
|
||||
text: str,
|
||||
index: int | None = None,
|
||||
) -> dict:
|
||||
"""
|
||||
Insert text at a specific index or at the end of the document.
|
||||
|
||||
Note: Google Docs uses 1-based indexing. Index 1 is the start of the document.
|
||||
|
||||
Args:
|
||||
document_id: The ID of the Google Docs document
|
||||
text: The text to insert
|
||||
index: The index where to insert text (1-based). If None, appends to end.
|
||||
|
||||
Returns:
|
||||
Dict with update result, or error
|
||||
"""
|
||||
client = _get_client()
|
||||
if isinstance(client, dict):
|
||||
return client
|
||||
try:
|
||||
return client.insert_text(document_id, text, index)
|
||||
except httpx.TimeoutException:
|
||||
return {"error": "Request timed out"}
|
||||
except httpx.RequestError as e:
|
||||
return {"error": f"Network error: {e}"}
|
||||
|
||||
@mcp.tool()
|
||||
def google_docs_replace_all_text(
|
||||
document_id: str,
|
||||
find_text: str,
|
||||
replace_text: str,
|
||||
match_case: bool = True,
|
||||
) -> dict:
|
||||
"""
|
||||
Global find-and-replace (ideal for populating templates with dynamic data).
|
||||
|
||||
Use this for template placeholders like {{Customer_Name}} or {{Date}}.
|
||||
|
||||
Args:
|
||||
document_id: The ID of the Google Docs document
|
||||
find_text: The text to find (e.g., "{{Customer_Name}}")
|
||||
replace_text: The text to replace with (e.g., "John Doe")
|
||||
match_case: Whether to match case exactly (default: True)
|
||||
|
||||
Returns:
|
||||
Dict with number of replacements made, or error
|
||||
"""
|
||||
client = _get_client()
|
||||
if isinstance(client, dict):
|
||||
return client
|
||||
try:
|
||||
result = client.replace_all_text(document_id, find_text, replace_text, match_case)
|
||||
if "error" not in result:
|
||||
# Extract replacement count from response
|
||||
replies = result.get("replies", [])
|
||||
occurrences = 0
|
||||
for reply in replies:
|
||||
replace_reply = reply.get("replaceAllText", {})
|
||||
occurrences += replace_reply.get("occurrencesChanged", 0)
|
||||
return {
|
||||
"document_id": document_id,
|
||||
"find_text": find_text,
|
||||
"replace_text": replace_text,
|
||||
"occurrences_replaced": occurrences,
|
||||
}
|
||||
return result
|
||||
except httpx.TimeoutException:
|
||||
return {"error": "Request timed out"}
|
||||
except httpx.RequestError as e:
|
||||
return {"error": f"Network error: {e}"}
|
||||
|
||||
@mcp.tool()
|
||||
def google_docs_insert_image(
|
||||
document_id: str,
|
||||
image_uri: str,
|
||||
index: int,
|
||||
width_pt: float | None = None,
|
||||
height_pt: float | None = None,
|
||||
) -> dict:
|
||||
"""
|
||||
Insert an image into the document body via URI.
|
||||
|
||||
Note: The image URI must be publicly accessible by Google's servers.
|
||||
|
||||
Args:
|
||||
document_id: The ID of the Google Docs document
|
||||
image_uri: Public URL of the image to insert
|
||||
index: The index where to insert the image (1-based)
|
||||
width_pt: Optional width in points
|
||||
height_pt: Optional height in points
|
||||
|
||||
Returns:
|
||||
Dict with update result, or error
|
||||
"""
|
||||
client = _get_client()
|
||||
if isinstance(client, dict):
|
||||
return client
|
||||
try:
|
||||
return client.insert_image(document_id, image_uri, index, width_pt, height_pt)
|
||||
except httpx.TimeoutException:
|
||||
return {"error": "Request timed out"}
|
||||
except httpx.RequestError as e:
|
||||
return {"error": f"Network error: {e}"}
|
||||
|
||||
@mcp.tool()
|
||||
def google_docs_format_text(
|
||||
document_id: str,
|
||||
start_index: int,
|
||||
end_index: int,
|
||||
bold: bool | None = None,
|
||||
italic: bool | None = None,
|
||||
underline: bool | None = None,
|
||||
font_size_pt: float | None = None,
|
||||
foreground_color_red: float | None = None,
|
||||
foreground_color_green: float | None = None,
|
||||
foreground_color_blue: float | None = None,
|
||||
) -> dict:
|
||||
"""
|
||||
Apply styling (bold, italic, font size, colors) to specific text ranges.
|
||||
|
||||
Args:
|
||||
document_id: The ID of the Google Docs document
|
||||
start_index: Start index of the text range (1-based, inclusive)
|
||||
end_index: End index of the text range (1-based, exclusive)
|
||||
bold: Set text to bold (True/False/None to skip)
|
||||
italic: Set text to italic (True/False/None to skip)
|
||||
underline: Set text to underlined (True/False/None to skip)
|
||||
font_size_pt: Font size in points (e.g., 12.0)
|
||||
foreground_color_red: Red component (0.0-1.0)
|
||||
foreground_color_green: Green component (0.0-1.0)
|
||||
foreground_color_blue: Blue component (0.0-1.0)
|
||||
|
||||
Returns:
|
||||
Dict with update result, or error
|
||||
"""
|
||||
client = _get_client()
|
||||
if isinstance(client, dict):
|
||||
return client
|
||||
|
||||
foreground_color = None
|
||||
if any(
|
||||
c is not None
|
||||
for c in [foreground_color_red, foreground_color_green, foreground_color_blue]
|
||||
):
|
||||
foreground_color = {
|
||||
"red": foreground_color_red or 0.0,
|
||||
"green": foreground_color_green or 0.0,
|
||||
"blue": foreground_color_blue or 0.0,
|
||||
}
|
||||
|
||||
try:
|
||||
return client.format_text(
|
||||
document_id,
|
||||
start_index,
|
||||
end_index,
|
||||
bold=bold,
|
||||
italic=italic,
|
||||
underline=underline,
|
||||
font_size_pt=font_size_pt,
|
||||
foreground_color=foreground_color,
|
||||
)
|
||||
except httpx.TimeoutException:
|
||||
return {"error": "Request timed out"}
|
||||
except httpx.RequestError as e:
|
||||
return {"error": f"Network error: {e}"}
|
||||
|
||||
@mcp.tool()
|
||||
def google_docs_batch_update(
|
||||
document_id: str,
|
||||
requests_json: str,
|
||||
) -> dict:
|
||||
"""
|
||||
Execute multiple requests (inserts, deletes, formatting) in a single atomic operation.
|
||||
|
||||
This is the most powerful tool for complex document modifications.
|
||||
See: https://developers.google.com/docs/api/reference/rest/v1/documents/batchUpdate
|
||||
|
||||
Args:
|
||||
document_id: The ID of the Google Docs document
|
||||
requests_json: JSON string containing an array of request objects
|
||||
|
||||
Returns:
|
||||
Dict with batch update result, or error
|
||||
"""
|
||||
client = _get_client()
|
||||
if isinstance(client, dict):
|
||||
return client
|
||||
try:
|
||||
requests = json.loads(requests_json)
|
||||
if not isinstance(requests, list):
|
||||
return {"error": "requests_json must be a JSON array of request objects"}
|
||||
return client.batch_update(document_id, requests)
|
||||
except json.JSONDecodeError as e:
|
||||
return {"error": f"Invalid JSON: {e}"}
|
||||
except httpx.TimeoutException:
|
||||
return {"error": "Request timed out"}
|
||||
except httpx.RequestError as e:
|
||||
return {"error": f"Network error: {e}"}
|
||||
|
||||
@mcp.tool()
|
||||
def google_docs_create_list(
|
||||
document_id: str,
|
||||
start_index: int,
|
||||
end_index: int,
|
||||
list_type: str = "bullet",
|
||||
) -> dict:
|
||||
"""
|
||||
Create or modify bulleted and numbered lists within the document.
|
||||
|
||||
Args:
|
||||
document_id: The ID of the Google Docs document
|
||||
start_index: Start index of the paragraphs to convert (1-based)
|
||||
end_index: End index of the paragraphs to convert (1-based)
|
||||
list_type: Type of list - "bullet" or "numbered"
|
||||
|
||||
Returns:
|
||||
Dict with update result, or error
|
||||
"""
|
||||
client = _get_client()
|
||||
if isinstance(client, dict):
|
||||
return client
|
||||
|
||||
bullet_presets = {
|
||||
"bullet": "BULLET_DISC_CIRCLE_SQUARE",
|
||||
"numbered": "NUMBERED_DECIMAL_ALPHA_ROMAN",
|
||||
}
|
||||
preset = bullet_presets.get(list_type.lower(), "BULLET_DISC_CIRCLE_SQUARE")
|
||||
|
||||
try:
|
||||
return client.create_list(document_id, start_index, end_index, preset)
|
||||
except httpx.TimeoutException:
|
||||
return {"error": "Request timed out"}
|
||||
except httpx.RequestError as e:
|
||||
return {"error": f"Network error: {e}"}
|
||||
|
||||
@mcp.tool()
|
||||
def google_docs_add_comment(
|
||||
document_id: str,
|
||||
content: str,
|
||||
quoted_text: str | None = None,
|
||||
) -> dict:
|
||||
"""
|
||||
Create a comment or anchor a discussion thread to a specific text segment.
|
||||
|
||||
Note: This uses the Google Drive API for comments.
|
||||
|
||||
Args:
|
||||
document_id: The ID of the Google Docs document
|
||||
content: The comment text
|
||||
quoted_text: Optional text from the document to anchor the comment to
|
||||
|
||||
Returns:
|
||||
Dict with comment details, or error
|
||||
"""
|
||||
client = _get_client()
|
||||
if isinstance(client, dict):
|
||||
return client
|
||||
try:
|
||||
return client.add_comment(document_id, content, quoted_text)
|
||||
except httpx.TimeoutException:
|
||||
return {"error": "Request timed out"}
|
||||
except httpx.RequestError as e:
|
||||
return {"error": f"Network error: {e}"}
|
||||
|
||||
@mcp.tool()
|
||||
def google_docs_export_content(
|
||||
document_id: str,
|
||||
format: str = "pdf",
|
||||
) -> dict:
|
||||
"""
|
||||
Export the document to different formats (PDF, DOCX, TXT).
|
||||
|
||||
Args:
|
||||
document_id: The ID of the Google Docs document
|
||||
format: Export format - "pdf", "docx", "txt", "html", "odt", "rtf", "epub"
|
||||
|
||||
Returns:
|
||||
Dict with base64-encoded content and metadata, or error
|
||||
"""
|
||||
client = _get_client()
|
||||
if isinstance(client, dict):
|
||||
return client
|
||||
|
||||
mime_types = {
|
||||
"pdf": "application/pdf",
|
||||
"docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
||||
"txt": "text/plain",
|
||||
"html": "text/html",
|
||||
"odt": "application/vnd.oasis.opendocument.text",
|
||||
"rtf": "application/rtf",
|
||||
"epub": "application/epub+zip",
|
||||
}
|
||||
mime_type = mime_types.get(format.lower(), "application/pdf")
|
||||
|
||||
try:
|
||||
return client.export_document(document_id, mime_type)
|
||||
except httpx.TimeoutException:
|
||||
return {"error": "Request timed out"}
|
||||
except httpx.RequestError as e:
|
||||
return {"error": f"Network error: {e}"}
|
||||
@@ -0,0 +1 @@
|
||||
"""Tests for Google Docs tool."""
|
||||
@@ -0,0 +1,350 @@
|
||||
"""
|
||||
Tests for Google Docs Tool.
|
||||
|
||||
These tests use mocked HTTP responses to verify the tool's behavior
|
||||
without requiring actual Google API credentials.
|
||||
"""
|
||||
|
||||
import json
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
from fastmcp import FastMCP
|
||||
|
||||
from aden_tools.tools.google_docs_tool import register_tools
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mcp():
|
||||
"""Create a FastMCP instance with Google Docs tools registered."""
|
||||
server = FastMCP("test")
|
||||
register_tools(server)
|
||||
return server
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mcp_with_credentials():
|
||||
"""Create a FastMCP instance with mocked credentials."""
|
||||
server = FastMCP("test")
|
||||
mock_credentials = MagicMock()
|
||||
mock_credentials.get.return_value = "test-access-token"
|
||||
register_tools(server, credentials=mock_credentials)
|
||||
return server
|
||||
|
||||
|
||||
def get_tool_fn(mcp, tool_name: str):
|
||||
"""Helper to get a tool function from the MCP server."""
|
||||
return mcp._tool_manager._tools[tool_name].fn
|
||||
|
||||
|
||||
class TestGoogleDocsCreateDocument:
|
||||
"""Tests for google_docs_create_document tool."""
|
||||
|
||||
def test_no_credentials_returns_error(self, mcp):
|
||||
"""Test that missing credentials returns a helpful error."""
|
||||
with patch.dict("os.environ", {}, clear=True):
|
||||
tool_fn = get_tool_fn(mcp, "google_docs_create_document")
|
||||
result = tool_fn(title="Test Document")
|
||||
assert "error" in result
|
||||
assert "not configured" in result["error"]
|
||||
assert "help" in result
|
||||
|
||||
@patch("httpx.post")
|
||||
def test_create_document_success(self, mock_post, mcp_with_credentials):
|
||||
"""Test successful document creation."""
|
||||
mock_response = MagicMock()
|
||||
mock_response.status_code = 200
|
||||
mock_response.json.return_value = {
|
||||
"documentId": "doc123",
|
||||
"title": "Test Document",
|
||||
}
|
||||
mock_post.return_value = mock_response
|
||||
|
||||
tool_fn = get_tool_fn(mcp_with_credentials, "google_docs_create_document")
|
||||
result = tool_fn(title="Test Document")
|
||||
|
||||
assert result["document_id"] == "doc123"
|
||||
assert result["title"] == "Test Document"
|
||||
assert "document_url" in result
|
||||
assert "doc123" in result["document_url"]
|
||||
|
||||
@patch("httpx.post")
|
||||
def test_create_document_unauthorized(self, mock_post, mcp_with_credentials):
|
||||
"""Test handling of 401 unauthorized response."""
|
||||
mock_response = MagicMock()
|
||||
mock_response.status_code = 401
|
||||
mock_post.return_value = mock_response
|
||||
|
||||
tool_fn = get_tool_fn(mcp_with_credentials, "google_docs_create_document")
|
||||
result = tool_fn(title="Test Document")
|
||||
|
||||
assert "error" in result
|
||||
assert "expired" in result["error"].lower() or "invalid" in result["error"].lower()
|
||||
|
||||
|
||||
class TestGoogleDocsGetDocument:
|
||||
"""Tests for google_docs_get_document tool."""
|
||||
|
||||
@patch("httpx.get")
|
||||
def test_get_document_success(self, mock_get, mcp_with_credentials):
|
||||
"""Test successful document retrieval."""
|
||||
mock_response = MagicMock()
|
||||
mock_response.status_code = 200
|
||||
mock_response.json.return_value = {
|
||||
"documentId": "doc123",
|
||||
"title": "Test Document",
|
||||
"body": {"content": []},
|
||||
}
|
||||
mock_get.return_value = mock_response
|
||||
|
||||
tool_fn = get_tool_fn(mcp_with_credentials, "google_docs_get_document")
|
||||
result = tool_fn(document_id="doc123")
|
||||
|
||||
assert result["documentId"] == "doc123"
|
||||
assert result["title"] == "Test Document"
|
||||
|
||||
@patch("httpx.get")
|
||||
def test_get_document_not_found(self, mock_get, mcp_with_credentials):
|
||||
"""Test handling of 404 not found response."""
|
||||
mock_response = MagicMock()
|
||||
mock_response.status_code = 404
|
||||
mock_get.return_value = mock_response
|
||||
|
||||
tool_fn = get_tool_fn(mcp_with_credentials, "google_docs_get_document")
|
||||
result = tool_fn(document_id="nonexistent")
|
||||
|
||||
assert "error" in result
|
||||
assert "not found" in result["error"].lower()
|
||||
|
||||
|
||||
class TestGoogleDocsReplaceAllText:
|
||||
"""Tests for google_docs_replace_all_text tool."""
|
||||
|
||||
@patch("httpx.post")
|
||||
def test_replace_all_text_success(self, mock_post, mcp_with_credentials):
|
||||
"""Test successful find and replace."""
|
||||
mock_response = MagicMock()
|
||||
mock_response.status_code = 200
|
||||
mock_response.json.return_value = {
|
||||
"replies": [{"replaceAllText": {"occurrencesChanged": 3}}]
|
||||
}
|
||||
mock_post.return_value = mock_response
|
||||
|
||||
tool_fn = get_tool_fn(mcp_with_credentials, "google_docs_replace_all_text")
|
||||
result = tool_fn(
|
||||
document_id="doc123",
|
||||
find_text="{{placeholder}}",
|
||||
replace_text="actual value",
|
||||
)
|
||||
|
||||
assert result["occurrences_replaced"] == 3
|
||||
assert result["find_text"] == "{{placeholder}}"
|
||||
assert result["replace_text"] == "actual value"
|
||||
|
||||
|
||||
class TestGoogleDocsInsertText:
|
||||
"""Tests for google_docs_insert_text tool."""
|
||||
|
||||
@patch("httpx.post")
|
||||
@patch("httpx.get")
|
||||
def test_insert_text_at_end(self, mock_get, mock_post, mcp_with_credentials):
|
||||
"""Test inserting text at the end of document."""
|
||||
# Mock get document for finding end index
|
||||
mock_get_response = MagicMock()
|
||||
mock_get_response.status_code = 200
|
||||
mock_get_response.json.return_value = {
|
||||
"body": {"content": [{"endIndex": 100}]}
|
||||
}
|
||||
mock_get.return_value = mock_get_response
|
||||
|
||||
# Mock batch update
|
||||
mock_post_response = MagicMock()
|
||||
mock_post_response.status_code = 200
|
||||
mock_post_response.json.return_value = {"replies": []}
|
||||
mock_post.return_value = mock_post_response
|
||||
|
||||
tool_fn = get_tool_fn(mcp_with_credentials, "google_docs_insert_text")
|
||||
result = tool_fn(document_id="doc123", text="Hello, World!")
|
||||
|
||||
assert "error" not in result
|
||||
|
||||
@patch("httpx.post")
|
||||
def test_insert_text_at_index(self, mock_post, mcp_with_credentials):
|
||||
"""Test inserting text at a specific index."""
|
||||
mock_response = MagicMock()
|
||||
mock_response.status_code = 200
|
||||
mock_response.json.return_value = {"replies": []}
|
||||
mock_post.return_value = mock_response
|
||||
|
||||
tool_fn = get_tool_fn(mcp_with_credentials, "google_docs_insert_text")
|
||||
result = tool_fn(document_id="doc123", text="Inserted", index=10)
|
||||
|
||||
assert "error" not in result
|
||||
|
||||
|
||||
class TestGoogleDocsFormatText:
|
||||
"""Tests for google_docs_format_text tool."""
|
||||
|
||||
@patch("httpx.post")
|
||||
def test_format_text_bold(self, mock_post, mcp_with_credentials):
|
||||
"""Test applying bold formatting."""
|
||||
mock_response = MagicMock()
|
||||
mock_response.status_code = 200
|
||||
mock_response.json.return_value = {"replies": []}
|
||||
mock_post.return_value = mock_response
|
||||
|
||||
tool_fn = get_tool_fn(mcp_with_credentials, "google_docs_format_text")
|
||||
result = tool_fn(
|
||||
document_id="doc123",
|
||||
start_index=1,
|
||||
end_index=10,
|
||||
bold=True,
|
||||
)
|
||||
|
||||
assert "error" not in result
|
||||
|
||||
def test_format_text_no_options(self, mcp_with_credentials):
|
||||
"""Test error when no formatting options specified."""
|
||||
tool_fn = get_tool_fn(mcp_with_credentials, "google_docs_format_text")
|
||||
result = tool_fn(
|
||||
document_id="doc123",
|
||||
start_index=1,
|
||||
end_index=10,
|
||||
)
|
||||
|
||||
assert "error" in result
|
||||
assert "No formatting options" in result["error"]
|
||||
|
||||
|
||||
class TestGoogleDocsBatchUpdate:
|
||||
"""Tests for google_docs_batch_update tool."""
|
||||
|
||||
@patch("httpx.post")
|
||||
def test_batch_update_success(self, mock_post, mcp_with_credentials):
|
||||
"""Test successful batch update."""
|
||||
mock_response = MagicMock()
|
||||
mock_response.status_code = 200
|
||||
mock_response.json.return_value = {"replies": [{}, {}]}
|
||||
mock_post.return_value = mock_response
|
||||
|
||||
tool_fn = get_tool_fn(mcp_with_credentials, "google_docs_batch_update")
|
||||
requests = json.dumps([
|
||||
{"insertText": {"location": {"index": 1}, "text": "Hello"}},
|
||||
{"insertText": {"location": {"index": 6}, "text": " World"}},
|
||||
])
|
||||
result = tool_fn(document_id="doc123", requests_json=requests)
|
||||
|
||||
assert "error" not in result
|
||||
|
||||
def test_batch_update_invalid_json(self, mcp_with_credentials):
|
||||
"""Test error handling for invalid JSON."""
|
||||
tool_fn = get_tool_fn(mcp_with_credentials, "google_docs_batch_update")
|
||||
result = tool_fn(document_id="doc123", requests_json="not valid json")
|
||||
|
||||
assert "error" in result
|
||||
assert "Invalid JSON" in result["error"]
|
||||
|
||||
def test_batch_update_not_array(self, mcp_with_credentials):
|
||||
"""Test error handling when JSON is not an array."""
|
||||
tool_fn = get_tool_fn(mcp_with_credentials, "google_docs_batch_update")
|
||||
result = tool_fn(document_id="doc123", requests_json='{"not": "array"}')
|
||||
|
||||
assert "error" in result
|
||||
assert "array" in result["error"].lower()
|
||||
|
||||
|
||||
class TestGoogleDocsExport:
|
||||
"""Tests for google_docs_export_content tool."""
|
||||
|
||||
@patch("httpx.get")
|
||||
def test_export_to_pdf(self, mock_get, mcp_with_credentials):
|
||||
"""Test exporting document to PDF."""
|
||||
mock_response = MagicMock()
|
||||
mock_response.status_code = 200
|
||||
mock_response.content = b"PDF content here"
|
||||
mock_get.return_value = mock_response
|
||||
|
||||
tool_fn = get_tool_fn(mcp_with_credentials, "google_docs_export_content")
|
||||
result = tool_fn(document_id="doc123", format="pdf")
|
||||
|
||||
assert result["document_id"] == "doc123"
|
||||
assert result["mime_type"] == "application/pdf"
|
||||
assert "content_base64" in result
|
||||
assert result["size_bytes"] == len(b"PDF content here")
|
||||
|
||||
@patch("httpx.get")
|
||||
def test_export_to_docx(self, mock_get, mcp_with_credentials):
|
||||
"""Test exporting document to DOCX."""
|
||||
mock_response = MagicMock()
|
||||
mock_response.status_code = 200
|
||||
mock_response.content = b"DOCX content"
|
||||
mock_get.return_value = mock_response
|
||||
|
||||
tool_fn = get_tool_fn(mcp_with_credentials, "google_docs_export_content")
|
||||
result = tool_fn(document_id="doc123", format="docx")
|
||||
|
||||
assert "application/vnd.openxmlformats" in result["mime_type"]
|
||||
|
||||
|
||||
class TestGoogleDocsCreateList:
|
||||
"""Tests for google_docs_create_list tool."""
|
||||
|
||||
@patch("httpx.post")
|
||||
def test_create_bullet_list(self, mock_post, mcp_with_credentials):
|
||||
"""Test creating a bullet list."""
|
||||
mock_response = MagicMock()
|
||||
mock_response.status_code = 200
|
||||
mock_response.json.return_value = {"replies": []}
|
||||
mock_post.return_value = mock_response
|
||||
|
||||
tool_fn = get_tool_fn(mcp_with_credentials, "google_docs_create_list")
|
||||
result = tool_fn(
|
||||
document_id="doc123",
|
||||
start_index=1,
|
||||
end_index=50,
|
||||
list_type="bullet",
|
||||
)
|
||||
|
||||
assert "error" not in result
|
||||
|
||||
@patch("httpx.post")
|
||||
def test_create_numbered_list(self, mock_post, mcp_with_credentials):
|
||||
"""Test creating a numbered list."""
|
||||
mock_response = MagicMock()
|
||||
mock_response.status_code = 200
|
||||
mock_response.json.return_value = {"replies": []}
|
||||
mock_post.return_value = mock_response
|
||||
|
||||
tool_fn = get_tool_fn(mcp_with_credentials, "google_docs_create_list")
|
||||
result = tool_fn(
|
||||
document_id="doc123",
|
||||
start_index=1,
|
||||
end_index=50,
|
||||
list_type="numbered",
|
||||
)
|
||||
|
||||
assert "error" not in result
|
||||
|
||||
|
||||
class TestGoogleDocsAddComment:
|
||||
"""Tests for google_docs_add_comment tool."""
|
||||
|
||||
@patch("httpx.post")
|
||||
def test_add_comment_success(self, mock_post, mcp_with_credentials):
|
||||
"""Test adding a comment to a document."""
|
||||
mock_response = MagicMock()
|
||||
mock_response.status_code = 200
|
||||
mock_response.json.return_value = {
|
||||
"id": "comment123",
|
||||
"content": "This needs review",
|
||||
}
|
||||
mock_post.return_value = mock_response
|
||||
|
||||
tool_fn = get_tool_fn(mcp_with_credentials, "google_docs_add_comment")
|
||||
result = tool_fn(
|
||||
document_id="doc123",
|
||||
content="This needs review",
|
||||
)
|
||||
|
||||
assert result["id"] == "comment123"
|
||||
assert result["content"] == "This needs review"
|
||||
Reference in New Issue
Block a user