├── README.md ├── mcp_server ├── __init__.py ├── google_docs_service.py └── mcp_server.py ├── .python-version ├── .idea ├── encodings.xml ├── vcs.xml ├── .gitignore ├── modules.xml ├── mcp-google-docs.iml └── misc.xml ├── .gitignore ├── pyproject.toml ├── tests └── integration.py └── uv.lock /README.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /mcp_server/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.python-version: -------------------------------------------------------------------------------- 1 | 3.13 2 | -------------------------------------------------------------------------------- /.idea/encodings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | # Editor-based HTTP Client requests 5 | /httpRequests/ 6 | # Datasource local storage ignored files 7 | /dataSources/ 8 | /dataSources.local.xml 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Python-generated files 2 | __pycache__/ 3 | *.py[oc] 4 | build/ 5 | dist/ 6 | wheels/ 7 | *.egg-info 8 | 9 | # Virtual environments 10 | .venv 11 | 12 | *googleusercontent.com.json 13 | .auth 14 | .env 15 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/mcp-google-docs.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "mcp-google-docs" 3 | version = "0.2.14" 4 | description = "mcp server for viewing, editing, creating, google docs (supports comments)" 5 | readme = "README.md" 6 | requires-python = ">=3.13" 7 | dependencies = [ 8 | "google-api-python-client>=2.160.0", 9 | "google-auth>=2.38.0", 10 | "google-auth-oauthlib>=1.2.1", 11 | "httplib2>=0.22.0", 12 | "mcp>=1.2.1", 13 | "python-dotenv>=1.0.1", 14 | ] 15 | 16 | [dependency-groups] 17 | dev = [ 18 | "pytest-asyncio>=0.25.3", 19 | "pytest>=8.3.4", 20 | ] 21 | 22 | [tool] 23 | uv.package = true 24 | 25 | [project.scripts] 26 | server = "mcp_server.mcp_server:main" 27 | -------------------------------------------------------------------------------- /mcp_server/google_docs_service.py: -------------------------------------------------------------------------------- 1 | import os 2 | import asyncio 3 | import logging 4 | from google.auth.transport.requests import Request 5 | from google.oauth2.credentials import Credentials 6 | from google_auth_oauthlib.flow import InstalledAppFlow 7 | from googleapiclient.discovery import build 8 | 9 | # Set up logging 10 | logging.basicConfig(level=logging.INFO) 11 | logger = logging.getLogger(__name__) 12 | 13 | class GoogleDocsService: 14 | def __init__(self, creds_file_path: str, token_path: str, scopes: list[str] = None): 15 | # Default scopes include both Docs and Drive. 16 | if scopes is None: 17 | scopes = [ 18 | 'https://www.googleapis.com/auth/documents', 19 | 'https://www.googleapis.com/auth/drive' 20 | ] 21 | self.creds = self._get_credentials(creds_file_path, token_path, scopes) 22 | # Initialize the Docs API client. 23 | self.docs_service = build('docs', 'v1', credentials=self.creds) 24 | # Initialize the Drive API client (for sharing, comments, etc). 25 | self.drive_service = build('drive', 'v3', credentials=self.creds) 26 | logger.info("Google Docs and Drive services initialized.") 27 | 28 | def _get_credentials(self, creds_file_path: str, token_path: str, scopes: list[str]) -> Credentials: 29 | creds = None 30 | if os.path.exists(token_path): 31 | logger.info('Loading token from file.') 32 | creds = Credentials.from_authorized_user_file(token_path, scopes) 33 | if not creds or not creds.valid: 34 | if creds and creds.expired and creds.refresh_token: 35 | logger.info('Refreshing token.') 36 | creds.refresh(Request()) 37 | else: 38 | logger.info('Fetching new token.') 39 | flow = InstalledAppFlow.from_client_secrets_file(creds_file_path, scopes) 40 | creds = flow.run_local_server(port=0) 41 | with open(token_path, 'w') as token_file: 42 | token_file.write(creds.to_json()) 43 | logger.info(f'Token saved to {token_path}') 44 | return creds 45 | 46 | async def create_document(self, title: str = "New Document", org: str = None, role: str = "writer") -> dict: 47 | """ 48 | Creates a new Google Doc with the given title. 49 | If 'org' is provided (e.g., "example.com"), the document will be shared with everyone in that domain, 50 | using the specified role (default is "writer"). 51 | """ 52 | def _create(): 53 | body = {'title': title} 54 | return self.docs_service.documents().create(body=body).execute() 55 | doc = await asyncio.to_thread(_create) 56 | document_id = doc.get('documentId') 57 | logger.info(f"Created document with ID: {document_id}") 58 | 59 | if org and document_id: 60 | await self.share_document_with_org(document_id, org, role) 61 | return doc 62 | 63 | async def share_document_with_org(self, document_id: str, domain: str, role: str = "writer") -> dict: 64 | """ 65 | Shares the document with everyone in the specified domain. 66 | 67 | Args: 68 | document_id (str): The ID of the document to share. 69 | domain (str): Your organization's domain (e.g., "example.com"). 70 | role (str): The access level to grant ("writer" for editing, "reader" for viewing). 71 | 72 | Returns: 73 | dict: The response from the Drive API. 74 | """ 75 | def _share(): 76 | permission_body = { 77 | 'type': 'domain', 78 | 'role': role, 79 | 'domain': domain 80 | } 81 | return self.drive_service.permissions().create( 82 | fileId=document_id, 83 | body=permission_body, 84 | fields='id' 85 | ).execute() 86 | result = await asyncio.to_thread(_share) 87 | logger.info(f"Shared document {document_id} with organization domain: {domain}") 88 | return result 89 | 90 | async def edit_document(self, document_id: str, requests: list) -> dict: 91 | """Edits a document using a batchUpdate request.""" 92 | def _update(): 93 | body = {'requests': requests} 94 | return self.docs_service.documents().batchUpdate(documentId=document_id, body=body).execute() 95 | result = await asyncio.to_thread(_update) 96 | logger.info(f"Updated document {document_id}: {result}") 97 | return result 98 | 99 | async def read_document(self, document_id: str) -> dict: 100 | """Retrieves the entire Google Doc as a JSON structure.""" 101 | def _get_doc(): 102 | return self.docs_service.documents().get(documentId=document_id).execute() 103 | doc = await asyncio.to_thread(_get_doc) 104 | logger.info(f"Read document {document_id}") 105 | return doc 106 | 107 | def extract_text(self, doc: dict) -> str: 108 | """ 109 | Extracts and concatenates the plain text from the document's body content. 110 | This version strips trailing newline characters from each paragraph so that 111 | the resulting text matches the inserted content more precisely. 112 | """ 113 | content = doc.get('body', {}).get('content', []) 114 | paragraphs = [] 115 | for element in content: 116 | if 'paragraph' in element: 117 | para = '' 118 | for elem in element['paragraph'].get('elements', []): 119 | if 'textRun' in elem: 120 | para += elem['textRun'].get('content', '') 121 | # Remove any trailing newlines from the paragraph. 122 | paragraphs.append(para.rstrip("\n")) 123 | # Join paragraphs with a single newline and strip any trailing newline at the end. 124 | return "\n".join(paragraphs).rstrip("\n") 125 | 126 | async def read_document_text(self, document_id: str) -> str: 127 | """Convenience method to get the document text.""" 128 | doc = await self.read_document(document_id) 129 | return self.extract_text(doc) 130 | 131 | async def rewrite_document(self, document_id: str, final_text: str) -> dict: 132 | """ 133 | Rewrites the entire content of the document with the provided final text. 134 | It deletes the existing content (if any) and then inserts the final text at the start. 135 | """ 136 | # First, read the document to determine its current length. 137 | doc = await self.read_document(document_id) 138 | body_content = doc.get("body", {}).get("content", []) 139 | # Get the end index from the last element, defaulting to 1 if not found. 140 | end_index = body_content[-1].get("endIndex", 1) if body_content else 1 141 | 142 | requests = [] 143 | # Only delete content if there's something to remove. 144 | if end_index > 1 and (end_index - 1) > 1: 145 | requests.append({ 146 | "deleteContentRange": { 147 | "range": {"startIndex": 1, "endIndex": end_index - 1} 148 | } 149 | }) 150 | # Insert the final text at index 1. 151 | requests.append({ 152 | "insertText": {"location": {"index": 1}, "text": final_text} 153 | }) 154 | result = await self.edit_document(document_id, requests) 155 | return result 156 | 157 | async def read_comments(self, document_id: str) -> list: 158 | """Reads comments on the document using the Drive API.""" 159 | def _list_comments(): 160 | return self.drive_service.comments().list( 161 | fileId=document_id, 162 | fields="comments(id,content,author,createdTime,modifiedTime,resolved,replies(content,author,id,createdTime,modifiedTime))" 163 | ).execute() 164 | response = await asyncio.to_thread(_list_comments) 165 | comments = response.get('comments', []) 166 | logger.info(f"Retrieved {len(comments)} comments for document {document_id}") 167 | return comments 168 | 169 | async def reply_comment(self, document_id: str, comment_id: str, reply_content: str) -> dict: 170 | """Replies to a specific comment on a document using the Drive API.""" 171 | def _reply(): 172 | body = {'content': reply_content} 173 | return self.drive_service.replies().create( 174 | fileId=document_id, 175 | commentId=comment_id, 176 | body=body, 177 | fields="id,content,author,createdTime,modifiedTime" 178 | ).execute() 179 | reply = await asyncio.to_thread(_reply) 180 | logger.info(f"Posted reply to comment {comment_id} in document {document_id}") 181 | return reply 182 | 183 | async def create_comment(self, document_id: str, content: str) -> dict: 184 | """Creates a comment on the document.""" 185 | def _create_comment(): 186 | body = {"content": content} 187 | return self.drive_service.comments().create( 188 | fileId=document_id, 189 | body=body, 190 | fields="id,content,author,createdTime,modifiedTime" 191 | ).execute() 192 | comment = await asyncio.to_thread(_create_comment) 193 | logger.info(f"Created comment with ID: {comment.get('id')}") 194 | return comment 195 | 196 | async def delete_reply(self, document_id: str, comment_id: str, reply_id: str) -> dict: 197 | """Deletes a reply to a comment in a document using the Drive API.""" 198 | def _delete_reply(): 199 | return self.drive_service.replies().delete( 200 | fileId=document_id, 201 | commentId=comment_id, 202 | replyId=reply_id 203 | ).execute() 204 | result = await asyncio.to_thread(_delete_reply) 205 | logger.info(f"Deleted reply {reply_id} for comment {comment_id} in document {document_id}") 206 | return result 207 | -------------------------------------------------------------------------------- /tests/integration.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | import asyncio 4 | import time 5 | import pytest 6 | import dotenv 7 | from googleapiclient.errors import HttpError 8 | import pytest_asyncio 9 | 10 | dotenv.load_dotenv() 11 | 12 | from mcp_server.google_docs_service import GoogleDocsService 13 | 14 | # Helper function to normalize text by collapsing multiple newlines. 15 | def normalize_text(text: str) -> str: 16 | return re.sub(r'\n+', '\n', text).strip() 17 | 18 | # Async fixtures for credentials and token file paths. 19 | @pytest_asyncio.fixture(scope="session") 20 | def creds_file_path(): 21 | path = os.environ.get("GOOGLE_CREDS_FILE") 22 | if not path: 23 | pytest.skip("GOOGLE_CREDS_FILE environment variable not set") 24 | return path 25 | 26 | @pytest_asyncio.fixture(scope="session") 27 | def token_file_path(): 28 | path = os.environ.get("GOOGLE_TOKEN_FILE") 29 | if not path: 30 | pytest.skip("GOOGLE_TOKEN_FILE environment variable not set") 31 | return path 32 | 33 | @pytest_asyncio.fixture(scope="session") 34 | def docs_service(creds_file_path, token_file_path): 35 | service = GoogleDocsService(creds_file_path, token_file_path) 36 | return service 37 | 38 | # Fixture to create a temporary document and clean it up afterward. 39 | @pytest_asyncio.fixture 40 | async def temp_document(docs_service): 41 | unique_title = f"Integration Test Doc {int(time.time())}" 42 | doc = await docs_service.create_document(unique_title) 43 | document_id = doc.get("documentId") 44 | if not document_id: 45 | pytest.fail("Failed to create document.") 46 | yield document_id 47 | try: 48 | await asyncio.to_thread( 49 | lambda: docs_service.drive_service.files().delete(fileId=document_id).execute() 50 | ) 51 | except HttpError as e: 52 | print(f"Warning: Failed to delete document {document_id}: {e}") 53 | 54 | # Test: Create a new document. 55 | @pytest.mark.asyncio 56 | async def test_create_document(docs_service): 57 | title = f"Integration Create Doc Test {int(time.time())}" 58 | doc = await docs_service.create_document(title) 59 | document_id = doc.get("documentId") 60 | assert document_id, "Document ID should be returned on creation." 61 | read_doc = await docs_service.read_document(document_id) 62 | assert "body" in read_doc, "Document should contain a body." 63 | # Clean up. 64 | await asyncio.to_thread( 65 | lambda: docs_service.drive_service.files().delete(fileId=document_id).execute() 66 | ) 67 | 68 | # Test: Create a document and share with datastax.com 69 | @pytest.mark.asyncio 70 | async def test_create_document_with_org(docs_service): 71 | title = f"Integration Create Doc with Org Test {int(time.time())}" 72 | org = "datastax.com" 73 | # Create the document with org share enabled. 74 | doc = await docs_service.create_document(title, org, "writer") 75 | document_id = doc.get("documentId") 76 | assert document_id, "Document ID should be returned on creation with org sharing." 77 | # Give the permission a moment to propagate. 78 | await asyncio.sleep(1) 79 | # Retrieve the document's permissions. 80 | permissions = await asyncio.to_thread( 81 | lambda: docs_service.drive_service.permissions().list( 82 | fileId=document_id, 83 | fields="permissions(id, domain, role, type)" 84 | ).execute() 85 | ) 86 | # Look for a domain-level permission for datastax.com. 87 | domain_perms = [ 88 | perm for perm in permissions.get("permissions", []) 89 | if perm.get("type") == "domain" and perm.get("domain") == org 90 | ] 91 | assert domain_perms, f"Document should have domain permission for {org}." 92 | # Clean up. 93 | await asyncio.to_thread( 94 | lambda: docs_service.drive_service.files().delete(fileId=document_id).execute() 95 | ) 96 | 97 | # Test: Rewrite the document content. 98 | @pytest.mark.asyncio 99 | async def test_rewrite_document(temp_document, docs_service): 100 | document_id = temp_document 101 | final_text = ( 102 | "This is the new final content of the document.\n" 103 | "It has multiple lines.\n" 104 | "End of content." 105 | ) 106 | # Rewrite the document with the final text. 107 | result = await docs_service.rewrite_document(document_id, final_text) 108 | 109 | # Read back the document text. 110 | updated_text = await docs_service.read_document_text(document_id) 111 | 112 | # Normalize both expected and actual text to collapse extra newlines. 113 | normalized_expected = normalize_text(final_text) 114 | normalized_actual = normalize_text(updated_text) 115 | 116 | assert normalized_expected == normalized_actual, ( 117 | f"Expected: {normalized_expected}, but got: {normalized_actual}" 118 | ) 119 | 120 | # Test: Read document text. 121 | @pytest.mark.asyncio 122 | async def test_read_document_text(temp_document, docs_service): 123 | document_id = temp_document 124 | text = await docs_service.read_document_text(document_id) 125 | assert isinstance(text, str), "Document text should be a string" 126 | 127 | # Test: Read comments (even if none exist, should return a list). 128 | @pytest.mark.asyncio 129 | async def test_read_comments(temp_document, docs_service): 130 | document_id = temp_document 131 | comments = await docs_service.read_comments(document_id) 132 | assert isinstance(comments, list), "Comments should be returned as a list" 133 | 134 | # Test: Reply to a comment. 135 | @pytest.mark.asyncio 136 | async def test_reply_comment(temp_document, docs_service): 137 | document_id = temp_document 138 | # Create a comment. 139 | def create_comment(): 140 | body = {"content": "Integration test comment"} 141 | return docs_service.drive_service.comments().create( 142 | fileId=document_id, 143 | body=body, 144 | fields="id,content" 145 | ).execute() 146 | comment = await asyncio.to_thread(create_comment) 147 | comment_id = comment.get("id") 148 | assert comment_id, "A comment should have been created." 149 | 150 | # Post a reply. 151 | reply_text = "Integration test reply" 152 | reply_result = await docs_service.reply_comment(document_id, comment_id, reply_text) 153 | assert reply_text in reply_result.get("content", ""), "The reply should be posted." 154 | 155 | # Clean up: Delete the comment. 156 | def delete_comment(): 157 | return docs_service.drive_service.comments().delete( 158 | fileId=document_id, 159 | commentId=comment_id 160 | ).execute() 161 | await asyncio.to_thread(delete_comment) 162 | 163 | # Test: Create a comment. 164 | @pytest.mark.asyncio 165 | async def test_create_comment(temp_document, docs_service): 166 | document_id = temp_document 167 | content = "Test create comment" 168 | comment = await docs_service.create_comment(document_id, content) 169 | assert "id" in comment, "Created comment should have an ID." 170 | 171 | # Clean up: Delete the comment. 172 | def delete_comment(): 173 | return docs_service.drive_service.comments().delete( 174 | fileId=document_id, 175 | commentId=comment.get("id") 176 | ).execute() 177 | await asyncio.to_thread(delete_comment) 178 | 179 | # Test: Delete a reply. 180 | @pytest.mark.asyncio 181 | async def test_delete_reply(temp_document, docs_service): 182 | document_id = temp_document 183 | 184 | # Create a comment. 185 | def create_comment(): 186 | body = {"content": "Test comment for delete reply"} 187 | return docs_service.drive_service.comments().create( 188 | fileId=document_id, 189 | body=body, 190 | fields="id,content,replies" 191 | ).execute() 192 | comment = await asyncio.to_thread(create_comment) 193 | comment_id = comment.get("id") 194 | assert comment_id, "A comment should have been created for delete reply test." 195 | 196 | # Create a reply for the comment. 197 | reply_text = "Test reply to be deleted" 198 | reply = await docs_service.reply_comment(document_id, comment_id, reply_text) 199 | reply_id = reply.get("id") 200 | assert reply_id, "A reply should have been created for delete reply test." 201 | 202 | # Delete the reply using the new delete_reply method. 203 | await docs_service.delete_reply(document_id, comment_id, reply_id) 204 | 205 | # Verify the reply was deleted. 206 | comments = await docs_service.read_comments(document_id) 207 | for c in comments: 208 | if c.get("id") == comment_id: 209 | replies = c.get("replies", []) 210 | assert all(r.get("id") != reply_id for r in replies), "The reply should have been deleted." 211 | 212 | # Clean up: Delete the comment. 213 | def delete_comment(): 214 | return docs_service.drive_service.comments().delete( 215 | fileId=document_id, 216 | commentId=comment_id 217 | ).execute() 218 | await asyncio.to_thread(delete_comment) 219 | 220 | 221 | @pytest.mark.asyncio 222 | async def test_write_and_read_doc(): 223 | """ 224 | Integration test for creating a Google Doc, rewriting its content, 225 | and reading back the text to confirm the update. 226 | """ 227 | creds_file_path = os.environ.get("GOOGLE_CREDS_FILE") 228 | token_file = os.environ.get("GOOGLE_TOKEN_FILE") 229 | 230 | # Skip test if credentials are not set. 231 | if not creds_file_path or not token_file: 232 | pytest.skip("Environment variables GOOGLE_CREDS_FILE and GOOGLE_TOKEN_FILE must be set for integration tests.") 233 | 234 | # Instantiate the GoogleDocsService. 235 | service = GoogleDocsService(creds_file_path, token_file) 236 | 237 | # Create a new document with a test title. 238 | doc = await service.create_document(title="Integration Test Document") 239 | document_id = doc.get("documentId") 240 | assert document_id, "Document creation failed; no documentId returned." 241 | 242 | # Define the test content. 243 | test_content = "Hello, this is an integration test \nfor rewriting and reading a Google Doc!" 244 | 245 | # Rewrite the document with the test content. 246 | rewrite_result = await service.rewrite_document(document_id, test_content) 247 | 248 | # Read the document's text back. 249 | read_back_text = await service.read_document_text(document_id) 250 | 251 | # Verify that the new content appears in the document. 252 | assert test_content in read_back_text, ( 253 | f"Expected content not found in document. Read content: {read_back_text}" 254 | ) 255 | 256 | # Optionally, print the document URL for manual inspection. 257 | print(f"Test document URL: https://docs.google.com/document/d/{document_id}/edit") 258 | -------------------------------------------------------------------------------- /mcp_server/mcp_server.py: -------------------------------------------------------------------------------- 1 | import os 2 | import argparse 3 | import asyncio 4 | import dotenv 5 | 6 | # Import MCP server utilities. 7 | from mcp.server import Server, NotificationOptions 8 | from mcp.server.models import InitializationOptions 9 | import mcp.types as types 10 | import mcp.server.stdio 11 | 12 | # Import our updated GoogleDocsService. 13 | from mcp_server.google_docs_service import GoogleDocsService 14 | 15 | dotenv.load_dotenv() 16 | 17 | async def run_main(creds_file_path: str, token_path: str): 18 | # Convert relative paths to absolute paths. 19 | creds_file_path = os.path.abspath(creds_file_path) 20 | token_path = os.path.abspath(token_path) 21 | 22 | # Instantiate the service. 23 | docs_service = GoogleDocsService(creds_file_path, token_path) 24 | server = Server("googledocs") 25 | 26 | @server.list_tools() 27 | async def handle_list_tools() -> list[types.Tool]: 28 | return [ 29 | types.Tool( 30 | name="create-doc", 31 | description="Creates a new Google Doc with an optional title. Optionally, share with your organization by providing a domain and role.", 32 | inputSchema={ 33 | "type": "object", 34 | "properties": { 35 | "title": { 36 | "type": "string", 37 | "description": "Title of the new document", 38 | "default": "New Document", 39 | "example": "My New Document" 40 | }, 41 | "org": { 42 | "type": "string", 43 | "description": "Organization domain to share the document with (e.g., 'example.com')", 44 | "example": "example.com" 45 | }, 46 | "role": { 47 | "type": "string", 48 | "description": "Permission role to assign (e.g., 'writer' or 'reader')", 49 | "default": "writer", 50 | "example": "writer" 51 | } 52 | }, 53 | "required": [] 54 | } 55 | ), 56 | types.Tool( 57 | name="rewrite-document", 58 | description="Rewrites the entire content of a Google Doc with the provided final text", 59 | inputSchema={ 60 | "type": "object", 61 | "properties": { 62 | "document_id": { 63 | "type": "string", 64 | "description": "The ID of the Google Document", 65 | "example": "1abcXYZ..." 66 | }, 67 | "final_text": { 68 | "type": "string", 69 | "description": "The final text to replace the document's content", 70 | "example": "This is the new content of the document." 71 | } 72 | }, 73 | "required": ["document_id", "final_text"] 74 | } 75 | ), 76 | types.Tool( 77 | name="read-comments", 78 | description="Reads comments from a Google Doc", 79 | inputSchema={ 80 | "type": "object", 81 | "properties": { 82 | "document_id": { 83 | "type": "string", 84 | "description": "ID of the document", 85 | "example": "1abcXYZ..." 86 | } 87 | }, 88 | "required": ["document_id"] 89 | } 90 | ), 91 | types.Tool( 92 | name="reply-comment", 93 | description="Replies to a comment in a Google Doc", 94 | inputSchema={ 95 | "type": "object", 96 | "properties": { 97 | "document_id": { 98 | "type": "string", 99 | "description": "ID of the document", 100 | "example": "1abcXYZ..." 101 | }, 102 | "comment_id": { 103 | "type": "string", 104 | "description": "ID of the comment", 105 | "example": "Cp1..." 106 | }, 107 | "reply": { 108 | "type": "string", 109 | "description": "Content of the reply", 110 | "example": "Thanks for the feedback!" 111 | } 112 | }, 113 | "required": ["document_id", "comment_id", "reply"] 114 | } 115 | ), 116 | types.Tool( 117 | name="read-doc", 118 | description="Reads and returns the plain-text content of a Google Doc", 119 | inputSchema={ 120 | "type": "object", 121 | "properties": { 122 | "document_id": { 123 | "type": "string", 124 | "description": "ID of the document", 125 | "example": "1abcXYZ..." 126 | } 127 | }, 128 | "required": ["document_id"] 129 | } 130 | ), 131 | types.Tool( 132 | name="create-comment", 133 | description="Creates a new anchored comment on a Google Doc.", 134 | inputSchema={ 135 | "type": "object", 136 | "properties": { 137 | "document_id": { 138 | "type": "string", 139 | "description": "ID of the document", 140 | "example": "1abcXYZ..." 141 | }, 142 | "content": { 143 | "type": "string", 144 | "description": "The text content of the comment", 145 | "example": "This is an anchored comment." 146 | } 147 | }, 148 | "required": ["document_id", "content"] 149 | } 150 | ), 151 | types.Tool( 152 | name="delete-reply", 153 | description="Deletes a reply from a comment in a Google Doc", 154 | inputSchema={ 155 | "type": "object", 156 | "properties": { 157 | "document_id": { 158 | "type": "string", 159 | "description": "The ID of the Google Document", 160 | "example": "1abcXYZ..." 161 | }, 162 | "comment_id": { 163 | "type": "string", 164 | "description": "The ID of the comment containing the reply", 165 | "example": "Cp1..." 166 | }, 167 | "reply_id": { 168 | "type": "string", 169 | "description": "The ID of the reply to delete", 170 | "example": "reply123" 171 | } 172 | }, 173 | "required": ["document_id", "comment_id", "reply_id"] 174 | } 175 | ), 176 | ] 177 | 178 | @server.call_tool() 179 | async def handle_call_tool(name: str, arguments: dict | None) -> list[types.TextContent]: 180 | if name == "create-doc": 181 | title = arguments.get("title", "New Document") 182 | # Retrieve optional parameters for org and role. 183 | org = arguments.get("org") 184 | role = arguments.get("role", "writer") 185 | doc = await docs_service.create_document(title, org, role) 186 | return [types.TextContent( 187 | type="text", 188 | text=f"Document created at URL: https://docs.google.com/document/d/{doc.get('documentId')}/edit" 189 | )] 190 | elif name == "rewrite-document": 191 | document_id = arguments["document_id"] 192 | final_text = arguments["final_text"] 193 | result = await docs_service.rewrite_document(document_id, final_text) 194 | return [types.TextContent( 195 | type="text", 196 | text=f"Document {document_id} rewritten with new content. Result: {result}" 197 | )] 198 | elif name == "read-comments": 199 | document_id = arguments["document_id"] 200 | comments = await docs_service.read_comments(document_id) 201 | return [types.TextContent(type="text", text=str(comments))] 202 | elif name == "reply-comment": 203 | document_id = arguments["document_id"] 204 | comment_id = arguments["comment_id"] 205 | reply = arguments["reply"] 206 | result = await docs_service.reply_comment(document_id, comment_id, reply) 207 | return [types.TextContent(type="text", text=f"Reply posted: {result}")] 208 | elif name == "read-doc": 209 | document_id = arguments["document_id"] 210 | text = await docs_service.read_document_text(document_id) 211 | return [types.TextContent(type="text", text=text)] 212 | elif name == "create-comment": 213 | document_id = arguments["document_id"] 214 | content = arguments["content"] 215 | comment = await docs_service.create_comment(document_id, content) 216 | return [types.TextContent(type="text", text=f"Comment created: {comment}")] 217 | elif name == "delete-reply": 218 | document_id = arguments["document_id"] 219 | comment_id = arguments["comment_id"] 220 | reply_id = arguments["reply_id"] 221 | await docs_service.delete_reply(document_id, comment_id, reply_id) 222 | return [types.TextContent( 223 | type="text", 224 | text=f"Deleted reply {reply_id} from comment {comment_id} in document {document_id}." 225 | )] 226 | else: 227 | raise ValueError(f"Unknown tool: {name}") 228 | 229 | async with mcp.server.stdio.stdio_server() as (read_stream, write_stream): 230 | await server.run( 231 | read_stream, 232 | write_stream, 233 | InitializationOptions( 234 | server_name="googledocs", 235 | server_version="0.1.0", 236 | capabilities=server.get_capabilities( 237 | notification_options=NotificationOptions(), 238 | experimental_capabilities={} 239 | ), 240 | ), 241 | ) 242 | 243 | def main(): 244 | """ 245 | Entry point for the MCP server. Parses command-line arguments (or falls back to environment variables) 246 | for the credentials and token file paths, then calls the async run_main() function. 247 | """ 248 | parser = argparse.ArgumentParser(description='Google Docs API MCP Server') 249 | parser.add_argument( 250 | '--creds-file-path', '--creds_file_path', 251 | required=False, 252 | default=os.environ.get("GOOGLE_CREDS_FILE"), 253 | dest="creds_file_path", 254 | help='OAuth 2.0 credentials file path (or set GOOGLE_CREDS_FILE env variable)' 255 | ) 256 | parser.add_argument( 257 | '--token-path', '--token_path', 258 | required=False, 259 | default=os.environ.get("GOOGLE_TOKEN_FILE"), 260 | dest="token_path", 261 | help='File path to store/retrieve tokens (or set GOOGLE_TOKEN_FILE env variable)' 262 | ) 263 | args = parser.parse_args() 264 | if not args.creds_file_path or not args.token_path: 265 | parser.error("You must supply --creds-file-path and --token-path, or set GOOGLE_CREDS_FILE and GOOGLE_TOKEN_FILE environment variables.") 266 | asyncio.run(run_main(args.creds_file_path, args.token_path)) 267 | 268 | if __name__ == "__main__": 269 | main() 270 | -------------------------------------------------------------------------------- /uv.lock: -------------------------------------------------------------------------------- 1 | version = 1 2 | requires-python = ">=3.13" 3 | 4 | [[package]] 5 | name = "annotated-types" 6 | version = "0.7.0" 7 | source = { registry = "https://pypi.org/simple" } 8 | sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081 } 9 | wheels = [ 10 | { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643 }, 11 | ] 12 | 13 | [[package]] 14 | name = "anyio" 15 | version = "4.8.0" 16 | source = { registry = "https://pypi.org/simple" } 17 | dependencies = [ 18 | { name = "idna" }, 19 | { name = "sniffio" }, 20 | ] 21 | sdist = { url = "https://files.pythonhosted.org/packages/a3/73/199a98fc2dae33535d6b8e8e6ec01f8c1d76c9adb096c6b7d64823038cde/anyio-4.8.0.tar.gz", hash = "sha256:1d9fe889df5212298c0c0723fa20479d1b94883a2df44bd3897aa91083316f7a", size = 181126 } 22 | wheels = [ 23 | { url = "https://files.pythonhosted.org/packages/46/eb/e7f063ad1fec6b3178a3cd82d1a3c4de82cccf283fc42746168188e1cdd5/anyio-4.8.0-py3-none-any.whl", hash = "sha256:b5011f270ab5eb0abf13385f851315585cc37ef330dd88e27ec3d34d651fd47a", size = 96041 }, 24 | ] 25 | 26 | [[package]] 27 | name = "cachetools" 28 | version = "5.5.1" 29 | source = { registry = "https://pypi.org/simple" } 30 | sdist = { url = "https://files.pythonhosted.org/packages/d9/74/57df1ab0ce6bc5f6fa868e08de20df8ac58f9c44330c7671ad922d2bbeae/cachetools-5.5.1.tar.gz", hash = "sha256:70f238fbba50383ef62e55c6aff6d9673175fe59f7c6782c7a0b9e38f4a9df95", size = 28044 } 31 | wheels = [ 32 | { url = "https://files.pythonhosted.org/packages/ec/4e/de4ff18bcf55857ba18d3a4bd48c8a9fde6bb0980c9d20b263f05387fd88/cachetools-5.5.1-py3-none-any.whl", hash = "sha256:b76651fdc3b24ead3c648bbdeeb940c1b04d365b38b4af66788f9ec4a81d42bb", size = 9530 }, 33 | ] 34 | 35 | [[package]] 36 | name = "certifi" 37 | version = "2025.1.31" 38 | source = { registry = "https://pypi.org/simple" } 39 | sdist = { url = "https://files.pythonhosted.org/packages/1c/ab/c9f1e32b7b1bf505bf26f0ef697775960db7932abeb7b516de930ba2705f/certifi-2025.1.31.tar.gz", hash = "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651", size = 167577 } 40 | wheels = [ 41 | { url = "https://files.pythonhosted.org/packages/38/fc/bce832fd4fd99766c04d1ee0eead6b0ec6486fb100ae5e74c1d91292b982/certifi-2025.1.31-py3-none-any.whl", hash = "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe", size = 166393 }, 42 | ] 43 | 44 | [[package]] 45 | name = "charset-normalizer" 46 | version = "3.4.1" 47 | source = { registry = "https://pypi.org/simple" } 48 | sdist = { url = "https://files.pythonhosted.org/packages/16/b0/572805e227f01586461c80e0fd25d65a2115599cc9dad142fee4b747c357/charset_normalizer-3.4.1.tar.gz", hash = "sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3", size = 123188 } 49 | wheels = [ 50 | { url = "https://files.pythonhosted.org/packages/38/94/ce8e6f63d18049672c76d07d119304e1e2d7c6098f0841b51c666e9f44a0/charset_normalizer-3.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:aabfa34badd18f1da5ec1bc2715cadc8dca465868a4e73a0173466b688f29dda", size = 195698 }, 51 | { url = "https://files.pythonhosted.org/packages/24/2e/dfdd9770664aae179a96561cc6952ff08f9a8cd09a908f259a9dfa063568/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22e14b5d70560b8dd51ec22863f370d1e595ac3d024cb8ad7d308b4cd95f8313", size = 140162 }, 52 | { url = "https://files.pythonhosted.org/packages/24/4e/f646b9093cff8fc86f2d60af2de4dc17c759de9d554f130b140ea4738ca6/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8436c508b408b82d87dc5f62496973a1805cd46727c34440b0d29d8a2f50a6c9", size = 150263 }, 53 | { url = "https://files.pythonhosted.org/packages/5e/67/2937f8d548c3ef6e2f9aab0f6e21001056f692d43282b165e7c56023e6dd/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d074908e1aecee37a7635990b2c6d504cd4766c7bc9fc86d63f9c09af3fa11b", size = 142966 }, 54 | { url = "https://files.pythonhosted.org/packages/52/ed/b7f4f07de100bdb95c1756d3a4d17b90c1a3c53715c1a476f8738058e0fa/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:955f8851919303c92343d2f66165294848d57e9bba6cf6e3625485a70a038d11", size = 144992 }, 55 | { url = "https://files.pythonhosted.org/packages/96/2c/d49710a6dbcd3776265f4c923bb73ebe83933dfbaa841c5da850fe0fd20b/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:44ecbf16649486d4aebafeaa7ec4c9fed8b88101f4dd612dcaf65d5e815f837f", size = 147162 }, 56 | { url = "https://files.pythonhosted.org/packages/b4/41/35ff1f9a6bd380303dea55e44c4933b4cc3c4850988927d4082ada230273/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0924e81d3d5e70f8126529951dac65c1010cdf117bb75eb02dd12339b57749dd", size = 140972 }, 57 | { url = "https://files.pythonhosted.org/packages/fb/43/c6a0b685fe6910d08ba971f62cd9c3e862a85770395ba5d9cad4fede33ab/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2967f74ad52c3b98de4c3b32e1a44e32975e008a9cd2a8cc8966d6a5218c5cb2", size = 149095 }, 58 | { url = "https://files.pythonhosted.org/packages/4c/ff/a9a504662452e2d2878512115638966e75633519ec11f25fca3d2049a94a/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c75cb2a3e389853835e84a2d8fb2b81a10645b503eca9bcb98df6b5a43eb8886", size = 152668 }, 59 | { url = "https://files.pythonhosted.org/packages/6c/71/189996b6d9a4b932564701628af5cee6716733e9165af1d5e1b285c530ed/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:09b26ae6b1abf0d27570633b2b078a2a20419c99d66fb2823173d73f188ce601", size = 150073 }, 60 | { url = "https://files.pythonhosted.org/packages/e4/93/946a86ce20790e11312c87c75ba68d5f6ad2208cfb52b2d6a2c32840d922/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa88b843d6e211393a37219e6a1c1df99d35e8fd90446f1118f4216e307e48cd", size = 145732 }, 61 | { url = "https://files.pythonhosted.org/packages/cd/e5/131d2fb1b0dddafc37be4f3a2fa79aa4c037368be9423061dccadfd90091/charset_normalizer-3.4.1-cp313-cp313-win32.whl", hash = "sha256:eb8178fe3dba6450a3e024e95ac49ed3400e506fd4e9e5c32d30adda88cbd407", size = 95391 }, 62 | { url = "https://files.pythonhosted.org/packages/27/f2/4f9a69cc7712b9b5ad8fdb87039fd89abba997ad5cbe690d1835d40405b0/charset_normalizer-3.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:b1ac5992a838106edb89654e0aebfc24f5848ae2547d22c2c3f66454daa11971", size = 102702 }, 63 | { url = "https://files.pythonhosted.org/packages/0e/f6/65ecc6878a89bb1c23a086ea335ad4bf21a588990c3f535a227b9eea9108/charset_normalizer-3.4.1-py3-none-any.whl", hash = "sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85", size = 49767 }, 64 | ] 65 | 66 | [[package]] 67 | name = "click" 68 | version = "8.1.8" 69 | source = { registry = "https://pypi.org/simple" } 70 | dependencies = [ 71 | { name = "colorama", marker = "sys_platform == 'win32'" }, 72 | ] 73 | sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593 } 74 | wheels = [ 75 | { url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188 }, 76 | ] 77 | 78 | [[package]] 79 | name = "colorama" 80 | version = "0.4.6" 81 | source = { registry = "https://pypi.org/simple" } 82 | sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 } 83 | wheels = [ 84 | { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, 85 | ] 86 | 87 | [[package]] 88 | name = "google-api-core" 89 | version = "2.24.1" 90 | source = { registry = "https://pypi.org/simple" } 91 | dependencies = [ 92 | { name = "google-auth" }, 93 | { name = "googleapis-common-protos" }, 94 | { name = "proto-plus" }, 95 | { name = "protobuf" }, 96 | { name = "requests" }, 97 | ] 98 | sdist = { url = "https://files.pythonhosted.org/packages/b8/b7/481c83223d7b4f02c7651713fceca648fa3336e1571b9804713f66bca2d8/google_api_core-2.24.1.tar.gz", hash = "sha256:f8b36f5456ab0dd99a1b693a40a31d1e7757beea380ad1b38faaf8941eae9d8a", size = 163508 } 99 | wheels = [ 100 | { url = "https://files.pythonhosted.org/packages/b1/a6/8e30ddfd3d39ee6d2c76d3d4f64a83f77ac86a4cab67b286ae35ce9e4369/google_api_core-2.24.1-py3-none-any.whl", hash = "sha256:bc78d608f5a5bf853b80bd70a795f703294de656c096c0968320830a4bc280f1", size = 160059 }, 101 | ] 102 | 103 | [[package]] 104 | name = "google-api-python-client" 105 | version = "2.160.0" 106 | source = { registry = "https://pypi.org/simple" } 107 | dependencies = [ 108 | { name = "google-api-core" }, 109 | { name = "google-auth" }, 110 | { name = "google-auth-httplib2" }, 111 | { name = "httplib2" }, 112 | { name = "uritemplate" }, 113 | ] 114 | sdist = { url = "https://files.pythonhosted.org/packages/af/42/cbf81242376c99d6e5248e62aa4376bfde5bbefbe0a69b1b06fd4b73ab25/google_api_python_client-2.160.0.tar.gz", hash = "sha256:a8ccafaecfa42d15d5b5c3134ced8de08380019717fc9fb1ed510ca58eca3b7e", size = 12304236 } 115 | wheels = [ 116 | { url = "https://files.pythonhosted.org/packages/49/35/41623ac3b581781169eed7f5fcd24bc114c774dc491fab5c05d8eb81af36/google_api_python_client-2.160.0-py2.py3-none-any.whl", hash = "sha256:63d61fb3e4cf3fb31a70a87f45567c22f6dfe87bbfa27252317e3e2c42900db4", size = 12814302 }, 117 | ] 118 | 119 | [[package]] 120 | name = "google-auth" 121 | version = "2.38.0" 122 | source = { registry = "https://pypi.org/simple" } 123 | dependencies = [ 124 | { name = "cachetools" }, 125 | { name = "pyasn1-modules" }, 126 | { name = "rsa" }, 127 | ] 128 | sdist = { url = "https://files.pythonhosted.org/packages/c6/eb/d504ba1daf190af6b204a9d4714d457462b486043744901a6eeea711f913/google_auth-2.38.0.tar.gz", hash = "sha256:8285113607d3b80a3f1543b75962447ba8a09fe85783432a784fdeef6ac094c4", size = 270866 } 129 | wheels = [ 130 | { url = "https://files.pythonhosted.org/packages/9d/47/603554949a37bca5b7f894d51896a9c534b9eab808e2520a748e081669d0/google_auth-2.38.0-py2.py3-none-any.whl", hash = "sha256:e7dae6694313f434a2727bf2906f27ad259bae090d7aa896590d86feec3d9d4a", size = 210770 }, 131 | ] 132 | 133 | [[package]] 134 | name = "google-auth-httplib2" 135 | version = "0.2.0" 136 | source = { registry = "https://pypi.org/simple" } 137 | dependencies = [ 138 | { name = "google-auth" }, 139 | { name = "httplib2" }, 140 | ] 141 | sdist = { url = "https://files.pythonhosted.org/packages/56/be/217a598a818567b28e859ff087f347475c807a5649296fb5a817c58dacef/google-auth-httplib2-0.2.0.tar.gz", hash = "sha256:38aa7badf48f974f1eb9861794e9c0cb2a0511a4ec0679b1f886d108f5640e05", size = 10842 } 142 | wheels = [ 143 | { url = "https://files.pythonhosted.org/packages/be/8a/fe34d2f3f9470a27b01c9e76226965863f153d5fbe276f83608562e49c04/google_auth_httplib2-0.2.0-py2.py3-none-any.whl", hash = "sha256:b65a0a2123300dd71281a7bf6e64d65a0759287df52729bdd1ae2e47dc311a3d", size = 9253 }, 144 | ] 145 | 146 | [[package]] 147 | name = "google-auth-oauthlib" 148 | version = "1.2.1" 149 | source = { registry = "https://pypi.org/simple" } 150 | dependencies = [ 151 | { name = "google-auth" }, 152 | { name = "requests-oauthlib" }, 153 | ] 154 | sdist = { url = "https://files.pythonhosted.org/packages/cc/0f/1772edb8d75ecf6280f1c7f51cbcebe274e8b17878b382f63738fd96cee5/google_auth_oauthlib-1.2.1.tar.gz", hash = "sha256:afd0cad092a2eaa53cd8e8298557d6de1034c6cb4a740500b5357b648af97263", size = 24970 } 155 | wheels = [ 156 | { url = "https://files.pythonhosted.org/packages/1a/8e/22a28dfbd218033e4eeaf3a0533b2b54852b6530da0c0fe934f0cc494b29/google_auth_oauthlib-1.2.1-py2.py3-none-any.whl", hash = "sha256:2d58a27262d55aa1b87678c3ba7142a080098cbc2024f903c62355deb235d91f", size = 24930 }, 157 | ] 158 | 159 | [[package]] 160 | name = "googleapis-common-protos" 161 | version = "1.66.0" 162 | source = { registry = "https://pypi.org/simple" } 163 | dependencies = [ 164 | { name = "protobuf" }, 165 | ] 166 | sdist = { url = "https://files.pythonhosted.org/packages/ff/a7/8e9cccdb1c49870de6faea2a2764fa23f627dd290633103540209f03524c/googleapis_common_protos-1.66.0.tar.gz", hash = "sha256:c3e7b33d15fdca5374cc0a7346dd92ffa847425cc4ea941d970f13680052ec8c", size = 114376 } 167 | wheels = [ 168 | { url = "https://files.pythonhosted.org/packages/a0/0f/c0713fb2b3d28af4b2fded3291df1c4d4f79a00d15c2374a9e010870016c/googleapis_common_protos-1.66.0-py2.py3-none-any.whl", hash = "sha256:d7abcd75fabb2e0ec9f74466401f6c119a0b498e27370e9be4c94cb7e382b8ed", size = 221682 }, 169 | ] 170 | 171 | [[package]] 172 | name = "h11" 173 | version = "0.14.0" 174 | source = { registry = "https://pypi.org/simple" } 175 | sdist = { url = "https://files.pythonhosted.org/packages/f5/38/3af3d3633a34a3316095b39c8e8fb4853a28a536e55d347bd8d8e9a14b03/h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d", size = 100418 } 176 | wheels = [ 177 | { url = "https://files.pythonhosted.org/packages/95/04/ff642e65ad6b90db43e668d70ffb6736436c7ce41fcc549f4e9472234127/h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761", size = 58259 }, 178 | ] 179 | 180 | [[package]] 181 | name = "httpcore" 182 | version = "1.0.7" 183 | source = { registry = "https://pypi.org/simple" } 184 | dependencies = [ 185 | { name = "certifi" }, 186 | { name = "h11" }, 187 | ] 188 | sdist = { url = "https://files.pythonhosted.org/packages/6a/41/d7d0a89eb493922c37d343b607bc1b5da7f5be7e383740b4753ad8943e90/httpcore-1.0.7.tar.gz", hash = "sha256:8551cb62a169ec7162ac7be8d4817d561f60e08eaa485234898414bb5a8a0b4c", size = 85196 } 189 | wheels = [ 190 | { url = "https://files.pythonhosted.org/packages/87/f5/72347bc88306acb359581ac4d52f23c0ef445b57157adedb9aee0cd689d2/httpcore-1.0.7-py3-none-any.whl", hash = "sha256:a3fff8f43dc260d5bd363d9f9cf1830fa3a458b332856f34282de498ed420edd", size = 78551 }, 191 | ] 192 | 193 | [[package]] 194 | name = "httplib2" 195 | version = "0.22.0" 196 | source = { registry = "https://pypi.org/simple" } 197 | dependencies = [ 198 | { name = "pyparsing" }, 199 | ] 200 | sdist = { url = "https://files.pythonhosted.org/packages/3d/ad/2371116b22d616c194aa25ec410c9c6c37f23599dcd590502b74db197584/httplib2-0.22.0.tar.gz", hash = "sha256:d7a10bc5ef5ab08322488bde8c726eeee5c8618723fdb399597ec58f3d82df81", size = 351116 } 201 | wheels = [ 202 | { url = "https://files.pythonhosted.org/packages/a8/6c/d2fbdaaa5959339d53ba38e94c123e4e84b8fbc4b84beb0e70d7c1608486/httplib2-0.22.0-py3-none-any.whl", hash = "sha256:14ae0a53c1ba8f3d37e9e27cf37eabb0fb9980f435ba405d546948b009dd64dc", size = 96854 }, 203 | ] 204 | 205 | [[package]] 206 | name = "httpx" 207 | version = "0.28.1" 208 | source = { registry = "https://pypi.org/simple" } 209 | dependencies = [ 210 | { name = "anyio" }, 211 | { name = "certifi" }, 212 | { name = "httpcore" }, 213 | { name = "idna" }, 214 | ] 215 | sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406 } 216 | wheels = [ 217 | { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517 }, 218 | ] 219 | 220 | [[package]] 221 | name = "httpx-sse" 222 | version = "0.4.0" 223 | source = { registry = "https://pypi.org/simple" } 224 | sdist = { url = "https://files.pythonhosted.org/packages/4c/60/8f4281fa9bbf3c8034fd54c0e7412e66edbab6bc74c4996bd616f8d0406e/httpx-sse-0.4.0.tar.gz", hash = "sha256:1e81a3a3070ce322add1d3529ed42eb5f70817f45ed6ec915ab753f961139721", size = 12624 } 225 | wheels = [ 226 | { url = "https://files.pythonhosted.org/packages/e1/9b/a181f281f65d776426002f330c31849b86b31fc9d848db62e16f03ff739f/httpx_sse-0.4.0-py3-none-any.whl", hash = "sha256:f329af6eae57eaa2bdfd962b42524764af68075ea87370a2de920af5341e318f", size = 7819 }, 227 | ] 228 | 229 | [[package]] 230 | name = "idna" 231 | version = "3.10" 232 | source = { registry = "https://pypi.org/simple" } 233 | sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 } 234 | wheels = [ 235 | { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 }, 236 | ] 237 | 238 | [[package]] 239 | name = "iniconfig" 240 | version = "2.0.0" 241 | source = { registry = "https://pypi.org/simple" } 242 | sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646 } 243 | wheels = [ 244 | { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 }, 245 | ] 246 | 247 | [[package]] 248 | name = "mcp" 249 | version = "1.2.1" 250 | source = { registry = "https://pypi.org/simple" } 251 | dependencies = [ 252 | { name = "anyio" }, 253 | { name = "httpx" }, 254 | { name = "httpx-sse" }, 255 | { name = "pydantic" }, 256 | { name = "pydantic-settings" }, 257 | { name = "sse-starlette" }, 258 | { name = "starlette" }, 259 | { name = "uvicorn" }, 260 | ] 261 | sdist = { url = "https://files.pythonhosted.org/packages/fc/30/51e4555826126e3954fa2ab1e934bf74163c5fe05e98f38ca4d0f8abbf63/mcp-1.2.1.tar.gz", hash = "sha256:c9d43dbfe943aa1530e2be8f54b73af3ebfb071243827b4483d421684806cb45", size = 103968 } 262 | wheels = [ 263 | { url = "https://files.pythonhosted.org/packages/4c/0d/6770742a84c8aa1d36c0d628896a380584c5759612e66af7446af07d8775/mcp-1.2.1-py3-none-any.whl", hash = "sha256:579bf9c9157850ebb1344f3ca6f7a3021b0123c44c9f089ef577a7062522f0fd", size = 66453 }, 264 | ] 265 | 266 | [[package]] 267 | name = "mcp-google-docs" 268 | version = "0.2.2" 269 | source = { editable = "." } 270 | dependencies = [ 271 | { name = "google-api-python-client" }, 272 | { name = "google-auth" }, 273 | { name = "google-auth-oauthlib" }, 274 | { name = "httplib2" }, 275 | { name = "mcp" }, 276 | { name = "python-dotenv" }, 277 | ] 278 | 279 | [package.dev-dependencies] 280 | dev = [ 281 | { name = "pytest" }, 282 | { name = "pytest-asyncio" }, 283 | ] 284 | 285 | [package.metadata] 286 | requires-dist = [ 287 | { name = "google-api-python-client", specifier = ">=2.160.0" }, 288 | { name = "google-auth", specifier = ">=2.38.0" }, 289 | { name = "google-auth-oauthlib", specifier = ">=1.2.1" }, 290 | { name = "httplib2", specifier = ">=0.22.0" }, 291 | { name = "mcp", specifier = ">=1.2.1" }, 292 | { name = "python-dotenv", specifier = ">=1.0.1" }, 293 | ] 294 | 295 | [package.metadata.requires-dev] 296 | dev = [ 297 | { name = "pytest", specifier = ">=8.3.4" }, 298 | { name = "pytest-asyncio", specifier = ">=0.25.3" }, 299 | ] 300 | 301 | [[package]] 302 | name = "oauthlib" 303 | version = "3.2.2" 304 | source = { registry = "https://pypi.org/simple" } 305 | sdist = { url = "https://files.pythonhosted.org/packages/6d/fa/fbf4001037904031639e6bfbfc02badfc7e12f137a8afa254df6c4c8a670/oauthlib-3.2.2.tar.gz", hash = "sha256:9859c40929662bec5d64f34d01c99e093149682a3f38915dc0655d5a633dd918", size = 177352 } 306 | wheels = [ 307 | { url = "https://files.pythonhosted.org/packages/7e/80/cab10959dc1faead58dc8384a781dfbf93cb4d33d50988f7a69f1b7c9bbe/oauthlib-3.2.2-py3-none-any.whl", hash = "sha256:8139f29aac13e25d502680e9e19963e83f16838d48a0d71c287fe40e7067fbca", size = 151688 }, 308 | ] 309 | 310 | [[package]] 311 | name = "packaging" 312 | version = "24.2" 313 | source = { registry = "https://pypi.org/simple" } 314 | sdist = { url = "https://files.pythonhosted.org/packages/d0/63/68dbb6eb2de9cb10ee4c9c14a0148804425e13c4fb20d61cce69f53106da/packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f", size = 163950 } 315 | wheels = [ 316 | { url = "https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", size = 65451 }, 317 | ] 318 | 319 | [[package]] 320 | name = "pluggy" 321 | version = "1.5.0" 322 | source = { registry = "https://pypi.org/simple" } 323 | sdist = { url = "https://files.pythonhosted.org/packages/96/2d/02d4312c973c6050a18b314a5ad0b3210edb65a906f868e31c111dede4a6/pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", size = 67955 } 324 | wheels = [ 325 | { url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556 }, 326 | ] 327 | 328 | [[package]] 329 | name = "proto-plus" 330 | version = "1.26.0" 331 | source = { registry = "https://pypi.org/simple" } 332 | dependencies = [ 333 | { name = "protobuf" }, 334 | ] 335 | sdist = { url = "https://files.pythonhosted.org/packages/26/79/a5c6cbb42268cfd3ddc652dc526889044a8798c688a03ff58e5e92b743c8/proto_plus-1.26.0.tar.gz", hash = "sha256:6e93d5f5ca267b54300880fff156b6a3386b3fa3f43b1da62e680fc0c586ef22", size = 56136 } 336 | wheels = [ 337 | { url = "https://files.pythonhosted.org/packages/42/c3/59308ccc07b34980f9d532f7afc718a9f32b40e52cde7a740df8d55632fb/proto_plus-1.26.0-py3-none-any.whl", hash = "sha256:bf2dfaa3da281fc3187d12d224c707cb57214fb2c22ba854eb0c105a3fb2d4d7", size = 50166 }, 338 | ] 339 | 340 | [[package]] 341 | name = "protobuf" 342 | version = "5.29.3" 343 | source = { registry = "https://pypi.org/simple" } 344 | sdist = { url = "https://files.pythonhosted.org/packages/f7/d1/e0a911544ca9993e0f17ce6d3cc0932752356c1b0a834397f28e63479344/protobuf-5.29.3.tar.gz", hash = "sha256:5da0f41edaf117bde316404bad1a486cb4ededf8e4a54891296f648e8e076620", size = 424945 } 345 | wheels = [ 346 | { url = "https://files.pythonhosted.org/packages/dc/7a/1e38f3cafa022f477ca0f57a1f49962f21ad25850c3ca0acd3b9d0091518/protobuf-5.29.3-cp310-abi3-win32.whl", hash = "sha256:3ea51771449e1035f26069c4c7fd51fba990d07bc55ba80701c78f886bf9c888", size = 422708 }, 347 | { url = "https://files.pythonhosted.org/packages/61/fa/aae8e10512b83de633f2646506a6d835b151edf4b30d18d73afd01447253/protobuf-5.29.3-cp310-abi3-win_amd64.whl", hash = "sha256:a4fa6f80816a9a0678429e84973f2f98cbc218cca434abe8db2ad0bffc98503a", size = 434508 }, 348 | { url = "https://files.pythonhosted.org/packages/dd/04/3eaedc2ba17a088961d0e3bd396eac764450f431621b58a04ce898acd126/protobuf-5.29.3-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:a8434404bbf139aa9e1300dbf989667a83d42ddda9153d8ab76e0d5dcaca484e", size = 417825 }, 349 | { url = "https://files.pythonhosted.org/packages/4f/06/7c467744d23c3979ce250397e26d8ad8eeb2bea7b18ca12ad58313c1b8d5/protobuf-5.29.3-cp38-abi3-manylinux2014_aarch64.whl", hash = "sha256:daaf63f70f25e8689c072cfad4334ca0ac1d1e05a92fc15c54eb9cf23c3efd84", size = 319573 }, 350 | { url = "https://files.pythonhosted.org/packages/a8/45/2ebbde52ad2be18d3675b6bee50e68cd73c9e0654de77d595540b5129df8/protobuf-5.29.3-cp38-abi3-manylinux2014_x86_64.whl", hash = "sha256:c027e08a08be10b67c06bf2370b99c811c466398c357e615ca88c91c07f0910f", size = 319672 }, 351 | { url = "https://files.pythonhosted.org/packages/fd/b2/ab07b09e0f6d143dfb839693aa05765257bceaa13d03bf1a696b78323e7a/protobuf-5.29.3-py3-none-any.whl", hash = "sha256:0a18ed4a24198528f2333802eb075e59dea9d679ab7a6c5efb017a59004d849f", size = 172550 }, 352 | ] 353 | 354 | [[package]] 355 | name = "pyasn1" 356 | version = "0.6.1" 357 | source = { registry = "https://pypi.org/simple" } 358 | sdist = { url = "https://files.pythonhosted.org/packages/ba/e9/01f1a64245b89f039897cb0130016d79f77d52669aae6ee7b159a6c4c018/pyasn1-0.6.1.tar.gz", hash = "sha256:6f580d2bdd84365380830acf45550f2511469f673cb4a5ae3857a3170128b034", size = 145322 } 359 | wheels = [ 360 | { url = "https://files.pythonhosted.org/packages/c8/f1/d6a797abb14f6283c0ddff96bbdd46937f64122b8c925cab503dd37f8214/pyasn1-0.6.1-py3-none-any.whl", hash = "sha256:0d632f46f2ba09143da3a8afe9e33fb6f92fa2320ab7e886e2d0f7672af84629", size = 83135 }, 361 | ] 362 | 363 | [[package]] 364 | name = "pyasn1-modules" 365 | version = "0.4.1" 366 | source = { registry = "https://pypi.org/simple" } 367 | dependencies = [ 368 | { name = "pyasn1" }, 369 | ] 370 | sdist = { url = "https://files.pythonhosted.org/packages/1d/67/6afbf0d507f73c32d21084a79946bfcfca5fbc62a72057e9c23797a737c9/pyasn1_modules-0.4.1.tar.gz", hash = "sha256:c28e2dbf9c06ad61c71a075c7e0f9fd0f1b0bb2d2ad4377f240d33ac2ab60a7c", size = 310028 } 371 | wheels = [ 372 | { url = "https://files.pythonhosted.org/packages/77/89/bc88a6711935ba795a679ea6ebee07e128050d6382eaa35a0a47c8032bdc/pyasn1_modules-0.4.1-py3-none-any.whl", hash = "sha256:49bfa96b45a292b711e986f222502c1c9a5e1f4e568fc30e2574a6c7d07838fd", size = 181537 }, 373 | ] 374 | 375 | [[package]] 376 | name = "pydantic" 377 | version = "2.10.6" 378 | source = { registry = "https://pypi.org/simple" } 379 | dependencies = [ 380 | { name = "annotated-types" }, 381 | { name = "pydantic-core" }, 382 | { name = "typing-extensions" }, 383 | ] 384 | sdist = { url = "https://files.pythonhosted.org/packages/b7/ae/d5220c5c52b158b1de7ca89fc5edb72f304a70a4c540c84c8844bf4008de/pydantic-2.10.6.tar.gz", hash = "sha256:ca5daa827cce33de7a42be142548b0096bf05a7e7b365aebfa5f8eeec7128236", size = 761681 } 385 | wheels = [ 386 | { url = "https://files.pythonhosted.org/packages/f4/3c/8cc1cc84deffa6e25d2d0c688ebb80635dfdbf1dbea3e30c541c8cf4d860/pydantic-2.10.6-py3-none-any.whl", hash = "sha256:427d664bf0b8a2b34ff5dd0f5a18df00591adcee7198fbd71981054cef37b584", size = 431696 }, 387 | ] 388 | 389 | [[package]] 390 | name = "pydantic-core" 391 | version = "2.27.2" 392 | source = { registry = "https://pypi.org/simple" } 393 | dependencies = [ 394 | { name = "typing-extensions" }, 395 | ] 396 | sdist = { url = "https://files.pythonhosted.org/packages/fc/01/f3e5ac5e7c25833db5eb555f7b7ab24cd6f8c322d3a3ad2d67a952dc0abc/pydantic_core-2.27.2.tar.gz", hash = "sha256:eb026e5a4c1fee05726072337ff51d1efb6f59090b7da90d30ea58625b1ffb39", size = 413443 } 397 | wheels = [ 398 | { url = "https://files.pythonhosted.org/packages/41/b1/9bc383f48f8002f99104e3acff6cba1231b29ef76cfa45d1506a5cad1f84/pydantic_core-2.27.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:7d14bd329640e63852364c306f4d23eb744e0f8193148d4044dd3dacdaacbd8b", size = 1892709 }, 399 | { url = "https://files.pythonhosted.org/packages/10/6c/e62b8657b834f3eb2961b49ec8e301eb99946245e70bf42c8817350cbefc/pydantic_core-2.27.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:82f91663004eb8ed30ff478d77c4d1179b3563df6cdb15c0817cd1cdaf34d154", size = 1811273 }, 400 | { url = "https://files.pythonhosted.org/packages/ba/15/52cfe49c8c986e081b863b102d6b859d9defc63446b642ccbbb3742bf371/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71b24c7d61131bb83df10cc7e687433609963a944ccf45190cfc21e0887b08c9", size = 1823027 }, 401 | { url = "https://files.pythonhosted.org/packages/b1/1c/b6f402cfc18ec0024120602bdbcebc7bdd5b856528c013bd4d13865ca473/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fa8e459d4954f608fa26116118bb67f56b93b209c39b008277ace29937453dc9", size = 1868888 }, 402 | { url = "https://files.pythonhosted.org/packages/bd/7b/8cb75b66ac37bc2975a3b7de99f3c6f355fcc4d89820b61dffa8f1e81677/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce8918cbebc8da707ba805b7fd0b382816858728ae7fe19a942080c24e5b7cd1", size = 2037738 }, 403 | { url = "https://files.pythonhosted.org/packages/c8/f1/786d8fe78970a06f61df22cba58e365ce304bf9b9f46cc71c8c424e0c334/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eda3f5c2a021bbc5d976107bb302e0131351c2ba54343f8a496dc8783d3d3a6a", size = 2685138 }, 404 | { url = "https://files.pythonhosted.org/packages/a6/74/d12b2cd841d8724dc8ffb13fc5cef86566a53ed358103150209ecd5d1999/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd8086fa684c4775c27f03f062cbb9eaa6e17f064307e86b21b9e0abc9c0f02e", size = 1997025 }, 405 | { url = "https://files.pythonhosted.org/packages/a0/6e/940bcd631bc4d9a06c9539b51f070b66e8f370ed0933f392db6ff350d873/pydantic_core-2.27.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8d9b3388db186ba0c099a6d20f0604a44eabdeef1777ddd94786cdae158729e4", size = 2004633 }, 406 | { url = "https://files.pythonhosted.org/packages/50/cc/a46b34f1708d82498c227d5d80ce615b2dd502ddcfd8376fc14a36655af1/pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7a66efda2387de898c8f38c0cf7f14fca0b51a8ef0b24bfea5849f1b3c95af27", size = 1999404 }, 407 | { url = "https://files.pythonhosted.org/packages/ca/2d/c365cfa930ed23bc58c41463bae347d1005537dc8db79e998af8ba28d35e/pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:18a101c168e4e092ab40dbc2503bdc0f62010e95d292b27827871dc85450d7ee", size = 2130130 }, 408 | { url = "https://files.pythonhosted.org/packages/f4/d7/eb64d015c350b7cdb371145b54d96c919d4db516817f31cd1c650cae3b21/pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ba5dd002f88b78a4215ed2f8ddbdf85e8513382820ba15ad5ad8955ce0ca19a1", size = 2157946 }, 409 | { url = "https://files.pythonhosted.org/packages/a4/99/bddde3ddde76c03b65dfd5a66ab436c4e58ffc42927d4ff1198ffbf96f5f/pydantic_core-2.27.2-cp313-cp313-win32.whl", hash = "sha256:1ebaf1d0481914d004a573394f4be3a7616334be70261007e47c2a6fe7e50130", size = 1834387 }, 410 | { url = "https://files.pythonhosted.org/packages/71/47/82b5e846e01b26ac6f1893d3c5f9f3a2eb6ba79be26eef0b759b4fe72946/pydantic_core-2.27.2-cp313-cp313-win_amd64.whl", hash = "sha256:953101387ecf2f5652883208769a79e48db18c6df442568a0b5ccd8c2723abee", size = 1990453 }, 411 | { url = "https://files.pythonhosted.org/packages/51/b2/b2b50d5ecf21acf870190ae5d093602d95f66c9c31f9d5de6062eb329ad1/pydantic_core-2.27.2-cp313-cp313-win_arm64.whl", hash = "sha256:ac4dbfd1691affb8f48c2c13241a2e3b60ff23247cbcf981759c768b6633cf8b", size = 1885186 }, 412 | ] 413 | 414 | [[package]] 415 | name = "pydantic-settings" 416 | version = "2.7.1" 417 | source = { registry = "https://pypi.org/simple" } 418 | dependencies = [ 419 | { name = "pydantic" }, 420 | { name = "python-dotenv" }, 421 | ] 422 | sdist = { url = "https://files.pythonhosted.org/packages/73/7b/c58a586cd7d9ac66d2ee4ba60ca2d241fa837c02bca9bea80a9a8c3d22a9/pydantic_settings-2.7.1.tar.gz", hash = "sha256:10c9caad35e64bfb3c2fbf70a078c0e25cc92499782e5200747f942a065dec93", size = 79920 } 423 | wheels = [ 424 | { url = "https://files.pythonhosted.org/packages/b4/46/93416fdae86d40879714f72956ac14df9c7b76f7d41a4d68aa9f71a0028b/pydantic_settings-2.7.1-py3-none-any.whl", hash = "sha256:590be9e6e24d06db33a4262829edef682500ef008565a969c73d39d5f8bfb3fd", size = 29718 }, 425 | ] 426 | 427 | [[package]] 428 | name = "pyparsing" 429 | version = "3.2.1" 430 | source = { registry = "https://pypi.org/simple" } 431 | sdist = { url = "https://files.pythonhosted.org/packages/8b/1a/3544f4f299a47911c2ab3710f534e52fea62a633c96806995da5d25be4b2/pyparsing-3.2.1.tar.gz", hash = "sha256:61980854fd66de3a90028d679a954d5f2623e83144b5afe5ee86f43d762e5f0a", size = 1067694 } 432 | wheels = [ 433 | { url = "https://files.pythonhosted.org/packages/1c/a7/c8a2d361bf89c0d9577c934ebb7421b25dc84bf3a8e3ac0a40aed9acc547/pyparsing-3.2.1-py3-none-any.whl", hash = "sha256:506ff4f4386c4cec0590ec19e6302d3aedb992fdc02c761e90416f158dacf8e1", size = 107716 }, 434 | ] 435 | 436 | [[package]] 437 | name = "pytest" 438 | version = "8.3.4" 439 | source = { registry = "https://pypi.org/simple" } 440 | dependencies = [ 441 | { name = "colorama", marker = "sys_platform == 'win32'" }, 442 | { name = "iniconfig" }, 443 | { name = "packaging" }, 444 | { name = "pluggy" }, 445 | ] 446 | sdist = { url = "https://files.pythonhosted.org/packages/05/35/30e0d83068951d90a01852cb1cef56e5d8a09d20c7f511634cc2f7e0372a/pytest-8.3.4.tar.gz", hash = "sha256:965370d062bce11e73868e0335abac31b4d3de0e82f4007408d242b4f8610761", size = 1445919 } 447 | wheels = [ 448 | { url = "https://files.pythonhosted.org/packages/11/92/76a1c94d3afee238333bc0a42b82935dd8f9cf8ce9e336ff87ee14d9e1cf/pytest-8.3.4-py3-none-any.whl", hash = "sha256:50e16d954148559c9a74109af1eaf0c945ba2d8f30f0a3d3335edde19788b6f6", size = 343083 }, 449 | ] 450 | 451 | [[package]] 452 | name = "pytest-asyncio" 453 | version = "0.25.3" 454 | source = { registry = "https://pypi.org/simple" } 455 | dependencies = [ 456 | { name = "pytest" }, 457 | ] 458 | sdist = { url = "https://files.pythonhosted.org/packages/f2/a8/ecbc8ede70921dd2f544ab1cadd3ff3bf842af27f87bbdea774c7baa1d38/pytest_asyncio-0.25.3.tar.gz", hash = "sha256:fc1da2cf9f125ada7e710b4ddad05518d4cee187ae9412e9ac9271003497f07a", size = 54239 } 459 | wheels = [ 460 | { url = "https://files.pythonhosted.org/packages/67/17/3493c5624e48fd97156ebaec380dcaafee9506d7e2c46218ceebbb57d7de/pytest_asyncio-0.25.3-py3-none-any.whl", hash = "sha256:9e89518e0f9bd08928f97a3482fdc4e244df17529460bc038291ccaf8f85c7c3", size = 19467 }, 461 | ] 462 | 463 | [[package]] 464 | name = "python-dotenv" 465 | version = "1.0.1" 466 | source = { registry = "https://pypi.org/simple" } 467 | sdist = { url = "https://files.pythonhosted.org/packages/bc/57/e84d88dfe0aec03b7a2d4327012c1627ab5f03652216c63d49846d7a6c58/python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca", size = 39115 } 468 | wheels = [ 469 | { url = "https://files.pythonhosted.org/packages/6a/3e/b68c118422ec867fa7ab88444e1274aa40681c606d59ac27de5a5588f082/python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a", size = 19863 }, 470 | ] 471 | 472 | [[package]] 473 | name = "requests" 474 | version = "2.32.3" 475 | source = { registry = "https://pypi.org/simple" } 476 | dependencies = [ 477 | { name = "certifi" }, 478 | { name = "charset-normalizer" }, 479 | { name = "idna" }, 480 | { name = "urllib3" }, 481 | ] 482 | sdist = { url = "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", size = 131218 } 483 | wheels = [ 484 | { url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928 }, 485 | ] 486 | 487 | [[package]] 488 | name = "requests-oauthlib" 489 | version = "2.0.0" 490 | source = { registry = "https://pypi.org/simple" } 491 | dependencies = [ 492 | { name = "oauthlib" }, 493 | { name = "requests" }, 494 | ] 495 | sdist = { url = "https://files.pythonhosted.org/packages/42/f2/05f29bc3913aea15eb670be136045bf5c5bbf4b99ecb839da9b422bb2c85/requests-oauthlib-2.0.0.tar.gz", hash = "sha256:b3dffaebd884d8cd778494369603a9e7b58d29111bf6b41bdc2dcd87203af4e9", size = 55650 } 496 | wheels = [ 497 | { url = "https://files.pythonhosted.org/packages/3b/5d/63d4ae3b9daea098d5d6f5da83984853c1bbacd5dc826764b249fe119d24/requests_oauthlib-2.0.0-py2.py3-none-any.whl", hash = "sha256:7dd8a5c40426b779b0868c404bdef9768deccf22749cde15852df527e6269b36", size = 24179 }, 498 | ] 499 | 500 | [[package]] 501 | name = "rsa" 502 | version = "4.9" 503 | source = { registry = "https://pypi.org/simple" } 504 | dependencies = [ 505 | { name = "pyasn1" }, 506 | ] 507 | sdist = { url = "https://files.pythonhosted.org/packages/aa/65/7d973b89c4d2351d7fb232c2e452547ddfa243e93131e7cfa766da627b52/rsa-4.9.tar.gz", hash = "sha256:e38464a49c6c85d7f1351b0126661487a7e0a14a50f1675ec50eb34d4f20ef21", size = 29711 } 508 | wheels = [ 509 | { url = "https://files.pythonhosted.org/packages/49/97/fa78e3d2f65c02c8e1268b9aba606569fe97f6c8f7c2d74394553347c145/rsa-4.9-py3-none-any.whl", hash = "sha256:90260d9058e514786967344d0ef75fa8727eed8a7d2e43ce9f4bcf1b536174f7", size = 34315 }, 510 | ] 511 | 512 | [[package]] 513 | name = "sniffio" 514 | version = "1.3.1" 515 | source = { registry = "https://pypi.org/simple" } 516 | sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372 } 517 | wheels = [ 518 | { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 }, 519 | ] 520 | 521 | [[package]] 522 | name = "sse-starlette" 523 | version = "2.2.1" 524 | source = { registry = "https://pypi.org/simple" } 525 | dependencies = [ 526 | { name = "anyio" }, 527 | { name = "starlette" }, 528 | ] 529 | sdist = { url = "https://files.pythonhosted.org/packages/71/a4/80d2a11af59fe75b48230846989e93979c892d3a20016b42bb44edb9e398/sse_starlette-2.2.1.tar.gz", hash = "sha256:54470d5f19274aeed6b2d473430b08b4b379ea851d953b11d7f1c4a2c118b419", size = 17376 } 530 | wheels = [ 531 | { url = "https://files.pythonhosted.org/packages/d9/e0/5b8bd393f27f4a62461c5cf2479c75a2cc2ffa330976f9f00f5f6e4f50eb/sse_starlette-2.2.1-py3-none-any.whl", hash = "sha256:6410a3d3ba0c89e7675d4c273a301d64649c03a5ef1ca101f10b47f895fd0e99", size = 10120 }, 532 | ] 533 | 534 | [[package]] 535 | name = "starlette" 536 | version = "0.45.3" 537 | source = { registry = "https://pypi.org/simple" } 538 | dependencies = [ 539 | { name = "anyio" }, 540 | ] 541 | sdist = { url = "https://files.pythonhosted.org/packages/ff/fb/2984a686808b89a6781526129a4b51266f678b2d2b97ab2d325e56116df8/starlette-0.45.3.tar.gz", hash = "sha256:2cbcba2a75806f8a41c722141486f37c28e30a0921c5f6fe4346cb0dcee1302f", size = 2574076 } 542 | wheels = [ 543 | { url = "https://files.pythonhosted.org/packages/d9/61/f2b52e107b1fc8944b33ef56bf6ac4ebbe16d91b94d2b87ce013bf63fb84/starlette-0.45.3-py3-none-any.whl", hash = "sha256:dfb6d332576f136ec740296c7e8bb8c8a7125044e7c6da30744718880cdd059d", size = 71507 }, 544 | ] 545 | 546 | [[package]] 547 | name = "typing-extensions" 548 | version = "4.12.2" 549 | source = { registry = "https://pypi.org/simple" } 550 | sdist = { url = "https://files.pythonhosted.org/packages/df/db/f35a00659bc03fec321ba8bce9420de607a1d37f8342eee1863174c69557/typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8", size = 85321 } 551 | wheels = [ 552 | { url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438 }, 553 | ] 554 | 555 | [[package]] 556 | name = "uritemplate" 557 | version = "4.1.1" 558 | source = { registry = "https://pypi.org/simple" } 559 | sdist = { url = "https://files.pythonhosted.org/packages/d2/5a/4742fdba39cd02a56226815abfa72fe0aa81c33bed16ed045647d6000eba/uritemplate-4.1.1.tar.gz", hash = "sha256:4346edfc5c3b79f694bccd6d6099a322bbeb628dbf2cd86eea55a456ce5124f0", size = 273898 } 560 | wheels = [ 561 | { url = "https://files.pythonhosted.org/packages/81/c0/7461b49cd25aeece13766f02ee576d1db528f1c37ce69aee300e075b485b/uritemplate-4.1.1-py2.py3-none-any.whl", hash = "sha256:830c08b8d99bdd312ea4ead05994a38e8936266f84b9a7878232db50b044e02e", size = 10356 }, 562 | ] 563 | 564 | [[package]] 565 | name = "urllib3" 566 | version = "2.3.0" 567 | source = { registry = "https://pypi.org/simple" } 568 | sdist = { url = "https://files.pythonhosted.org/packages/aa/63/e53da845320b757bf29ef6a9062f5c669fe997973f966045cb019c3f4b66/urllib3-2.3.0.tar.gz", hash = "sha256:f8c5449b3cf0861679ce7e0503c7b44b5ec981bec0d1d3795a07f1ba96f0204d", size = 307268 } 569 | wheels = [ 570 | { url = "https://files.pythonhosted.org/packages/c8/19/4ec628951a74043532ca2cf5d97b7b14863931476d117c471e8e2b1eb39f/urllib3-2.3.0-py3-none-any.whl", hash = "sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df", size = 128369 }, 571 | ] 572 | 573 | [[package]] 574 | name = "uvicorn" 575 | version = "0.34.0" 576 | source = { registry = "https://pypi.org/simple" } 577 | dependencies = [ 578 | { name = "click" }, 579 | { name = "h11" }, 580 | ] 581 | sdist = { url = "https://files.pythonhosted.org/packages/4b/4d/938bd85e5bf2edeec766267a5015ad969730bb91e31b44021dfe8b22df6c/uvicorn-0.34.0.tar.gz", hash = "sha256:404051050cd7e905de2c9a7e61790943440b3416f49cb409f965d9dcd0fa73e9", size = 76568 } 582 | wheels = [ 583 | { url = "https://files.pythonhosted.org/packages/61/14/33a3a1352cfa71812a3a21e8c9bfb83f60b0011f5e36f2b1399d51928209/uvicorn-0.34.0-py3-none-any.whl", hash = "sha256:023dc038422502fa28a09c7a30bf2b6991512da7dcdb8fd35fe57cfc154126f4", size = 62315 }, 584 | ] 585 | --------------------------------------------------------------------------------