├── 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 |
5 |
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 |
--------------------------------------------------------------------------------