#
tokens: 13227/50000 15/15 files
lines: on (toggle) GitHub
raw markdown copy reset
# Directory Structure

```
├── .env.sample
├── .gitignore
├── Cargo.lock
├── Cargo.toml
├── LICENSE
├── README.md
└── src
    ├── auth.rs
    ├── client.rs
    ├── lib.rs
    ├── logging.rs
    ├── main.rs
    ├── servers
    │   ├── drive.rs
    │   ├── mod.rs
    │   └── sheets.rs
    └── tests
        ├── drive.rs
        ├── mod.rs
        └── sheets.rs
```

# Files

--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------

```
1 | /target
2 | .env
3 | 
```

--------------------------------------------------------------------------------
/.env.sample:
--------------------------------------------------------------------------------

```
1 | GOOGLE_ACCESS_TOKEN=sdfssdfdssf
2 | GOOGLE_REFRESH_TOKEN=sdfssdfdssf
3 | GOOGLE_CLIENT_ID=sdfssdfdssf
4 | GOOGLE_CLIENT_SECRET=sdfssdfdssf
5 | 
6 | 
7 | TEST_SPREADSHEET_ID="1yO2ZVWb-EEhv-xxxxxxx"
```

--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------

```markdown
  1 | # Google Drive & Sheets MCP Server
  2 | 
  3 | A Model Context Protocol (MCP) server built in Rust for interacting with Google Drive and Google Sheets. This tool provides MCP-compatible interfaces for Google Workspace services, allowing it to be used as part of larger agent workflows and compositions.
  4 | 
  5 | ## Features
  6 | 
  7 | ### Google Drive Operations
  8 | - List files in Google Drive with filtering options:
  9 |   - Filter by MIME type
 10 |   - Custom search queries
 11 |   - Configurable page size
 12 |   - Custom ordering
 13 | 
 14 | ### Google Sheets Operations
 15 | - Read data from Google Sheets with options:
 16 |   - Specify range
 17 |   - Choose major dimension (ROWS or COLUMNS)
 18 | - Write data to Google Sheets
 19 | - Create new spreadsheets with:
 20 |   - Custom title
 21 |   - Multiple sheets
 22 | - Clear values from ranges in spreadsheets
 23 | 
 24 | ## MCP Integration
 25 | 
 26 | This server implements the Model Context Protocol (MCP), making it compatible with agent frameworks like [Distri](https://github.com/distrihub/distri). Each service exposes its capabilities as MCP tools:
 27 | 
 28 | ### Drive Tools
 29 | - `list_files`: List and filter Drive files with customizable parameters
 30 | - Available capabilities exposed via `resources/list` endpoint
 31 | 
 32 | ### Sheets Tools
 33 | - `read_values`: Read spreadsheet data with dimension control
 34 | - `write_values`: Write data to spreadsheets
 35 | - `create_spreadsheet`: Create new spreadsheets
 36 | - `clear_values`: Clear ranges in spreadsheets
 37 | - Available capabilities exposed via `resources/list` endpoint
 38 | 
 39 | ## Prerequisites
 40 | 
 41 | - Rust (latest stable version)
 42 | - Google Cloud Project with Drive and Sheets APIs enabled
 43 | - OAuth 2.0 credentials configured for your Google Cloud Project
 44 | 
 45 | ## Installation
 46 | 
 47 | Install the `mcp-google` binary directly from GitHub using Cargo:
 48 | ```bash
 49 | cargo install --git https://github.com/distrihub/mcp-google-workspace.git
 50 | ```
 51 | 
 52 | This will install the `mcp-google` command to your system.
 53 | 
 54 | ## Configuration
 55 | 
 56 | Before using the server, you need to:
 57 | 
 58 | 1. Set up a Google Cloud Project
 59 | 2. Enable Google Drive and Google Sheets APIss
 60 | 3. Create OAuth 2.0 credentials
 61 | 4. Set up your environment variables:
 62 |    - `ACCESS_TOKEN`: Your Google OAuth access token
 63 |    - `GOOGLE_CLIENT_ID`: Your OAuth client ID
 64 |    - `GOOGLE_CLIENT_SECRET`: Your OAuth client secret
 65 |    - `GOOGLE_REFRESH_TOKEN`: Your OAuth refresh token
 66 | 
 67 | ## Usage
 68 | 
 69 | ### As MCP Server
 70 | 
 71 | The servers can be started independently and will communicate using the MCP protocol over stdio:
 72 | 
 73 | Start the Drive MCP server:
 74 | ```bash
 75 | mcp-google drive --access-token <your-access-token>
 76 | ```
 77 | 
 78 | Start the Sheets MCP server:
 79 | ```bash
 80 | mcp-google sheets --access-token <your-access-token>
 81 | ```
 82 | 
 83 | ### Using with Distri
 84 | 
 85 | This server can be used as part of a Distri agent configuration:
 86 | 
 87 | ```yaml
 88 | agents:
 89 |   google_workspace:
 90 |     drive:
 91 |       type: mcp
 92 |       command: ["mcp-google", "drive", "--access-token", "${ACCESS_TOKEN}"]
 93 |     sheets:
 94 |       type: mcp
 95 |       command: ["mcp-google", "sheets", "--access-token", "${ACCESS_TOKEN}"]
 96 | ```
 97 | 
 98 | ### Token Management
 99 | 
100 | Refresh your OAuth token:
101 | ```bash
102 | mcp-google refresh \
103 |   --client-id <your-client-id> \
104 |   --client-secret <your-client-secret> \
105 |   --refresh-token <your-refresh-token>
106 | ```
107 | 
108 | ## License
109 | 
110 | This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
111 | 
112 | ## Contributing
113 | 
114 | Contributions are welcome! Please feel free to submit a Pull Request.
115 | 
116 | ## Note
117 | 
118 | This is an MCP-compatible server that interacts with Google services. Make sure you have appropriate permissions and credentials before using the tool.
```

--------------------------------------------------------------------------------
/src/servers/mod.rs:
--------------------------------------------------------------------------------

```rust
1 | pub mod drive;
2 | pub mod sheets;
3 | 
```

--------------------------------------------------------------------------------
/src/tests/mod.rs:
--------------------------------------------------------------------------------

```rust
1 | pub mod drive;
2 | pub mod sheets;
3 | 
```

--------------------------------------------------------------------------------
/src/lib.rs:
--------------------------------------------------------------------------------

```rust
 1 | mod auth;
 2 | pub mod client;
 3 | pub mod logging;
 4 | pub mod servers;
 5 | 
 6 | #[cfg(test)]
 7 | mod tests;
 8 | 
 9 | // Re-export servers
10 | pub use auth::GoogleAuthService;
11 | use thiserror::Error;
12 | 
13 | #[derive(Debug, Error)]
14 | pub enum InvokeError {
15 |     #[error("Serde error: {0}")]
16 |     Serde(#[from] serde_json::Error),
17 | 
18 |     #[error("Environment variable missing: {0}")]
19 |     EnvVarMissing(String),
20 | 
21 |     #[error("Google API error: {0}")]
22 |     GoogleApi(String),
23 | 
24 |     #[error("Token parse error: {0}")]
25 |     TokenParse(String),
26 | 
27 |     #[error("User info error: {0}")]
28 |     UserInfo(String),
29 | 
30 |     #[error("JWT error: {0}")]
31 |     Jwt(String),
32 | }
33 | 
```

--------------------------------------------------------------------------------
/Cargo.toml:
--------------------------------------------------------------------------------

```toml
 1 | [package]
 2 | name = "mcp-google-workspace"
 3 | version = "0.1.0"
 4 | edition = "2021"
 5 | 
 6 | [dependencies]
 7 | async-mcp = { version = "0.1.0" }
 8 | tokio = { version = "1.0", features = ["full"] }
 9 | serde = { version = "1.0", features = ["derive"] }
10 | serde_json = "1.0"
11 | anyhow = "1.0"
12 | async-trait = "0.1"
13 | tracing-subscriber = { version = "0.3.0", features = [
14 |   "env-filter",
15 |   "tracing-log",
16 | ] }
17 | tracing = "0.1"
18 | url = "2.5.4"
19 | google-sheets4 = { version = "6.0", features = ["default"] }
20 | google-drive3 = "6.0.0"
21 | clap = { version = "4.4", features = ["derive", "env"] }
22 | reqwest = { version = "0.12.2", default-features = false, features = [
23 |   "json",
24 |   "stream",
25 | ] }
26 | chrono = { version = "0.4", features = ["serde"] }
27 | thiserror = "2.0.11"
28 | rand = "0.8"
29 | base64 = "0.21"
30 | urlencoding = "2.1.0"
31 | 
32 | [dev-dependencies]
33 | dotenv = "0.15"
34 | 
35 | [[bin]]
36 | name = "mcp-google"
37 | path = "src/main.rs"
38 | 
```

--------------------------------------------------------------------------------
/src/logging.rs:
--------------------------------------------------------------------------------

```rust
 1 | use tracing_subscriber::{filter::FilterFn, prelude::*, EnvFilter};
 2 | 
 3 | /// Initialize logging with sensible defaults for the agents library.
 4 | /// This will:
 5 | /// - Set up logging with the specified log level
 6 | /// - Filter out noisy logs from dependencies like hyper
 7 | /// - Format logs in a human-readable format
 8 | pub fn init_logging(level: &str) {
 9 |     let filter = EnvFilter::try_from_default_env()
10 |         .unwrap_or_else(|_| EnvFilter::new(level))
11 |         // Filter out noisy hyper logs
12 |         .add_directive("hyper=off".parse().unwrap())
13 |         .add_directive("rustyline=off".parse().unwrap())
14 |         .add_directive("h2=off".parse().unwrap())
15 |         .add_directive("rustls=off".parse().unwrap());
16 | 
17 |     // Only show our crate's logs and any errors from other crates
18 |     let _crate_filter = FilterFn::new(|metadata| {
19 |         metadata.target().starts_with("agents")
20 |             || metadata.target().starts_with("app")
21 |             || metadata.level() <= &tracing::Level::ERROR
22 |     });
23 | 
24 |     tracing_subscriber::registry()
25 |         .with(tracing_subscriber::fmt::layer().with_filter(filter))
26 |         // .with(filter)
27 |         .init();
28 | }
29 | 
```

--------------------------------------------------------------------------------
/src/client.rs:
--------------------------------------------------------------------------------

```rust
 1 | use google_drive3::DriveHub;
 2 | use google_sheets4::Sheets;
 3 | 
 4 | pub fn get_drive_client(
 5 |     access_token: &str,
 6 | ) -> DriveHub<
 7 |     google_drive3::hyper_rustls::HttpsConnector<
 8 |         google_drive3::hyper_util::client::legacy::connect::HttpConnector,
 9 |     >,
10 | > {
11 |     let hub = DriveHub::new(
12 |         google_drive3::hyper_util::client::legacy::Client::builder(
13 |             google_drive3::hyper_util::rt::TokioExecutor::new(),
14 |         )
15 |         .build(
16 |             google_sheets4::hyper_rustls::HttpsConnectorBuilder::new()
17 |                 .with_native_roots()
18 |                 .unwrap()
19 |                 .https_or_http()
20 |                 .enable_http1()
21 |                 .build(),
22 |         ),
23 |         access_token.to_string(),
24 |     );
25 |     hub
26 | }
27 | 
28 | pub fn get_sheets_client(
29 |     access_token: &str,
30 | ) -> Sheets<
31 |     google_sheets4::hyper_rustls::HttpsConnector<
32 |         google_sheets4::hyper_util::client::legacy::connect::HttpConnector,
33 |     >,
34 | > {
35 |     let hub = Sheets::new(
36 |         google_sheets4::hyper_util::client::legacy::Client::builder(
37 |             google_sheets4::hyper_util::rt::TokioExecutor::new(),
38 |         )
39 |         .build(
40 |             google_sheets4::hyper_rustls::HttpsConnectorBuilder::new()
41 |                 .with_native_roots()
42 |                 .unwrap()
43 |                 .https_or_http()
44 |                 .enable_http1()
45 |                 .build(),
46 |         ),
47 |         access_token.to_string(),
48 |     );
49 |     hub
50 | }
51 | 
```

--------------------------------------------------------------------------------
/src/main.rs:
--------------------------------------------------------------------------------

```rust
 1 | use anyhow::Result;
 2 | use async_mcp::transport::ServerStdioTransport;
 3 | use clap::{Parser, Subcommand};
 4 | use mcp_google_workspace::{
 5 |     logging::init_logging,
 6 |     servers::{drive, sheets},
 7 |     GoogleAuthService,
 8 | };
 9 | 
10 | #[derive(Parser)]
11 | #[command(author, version, about, long_about = None)]
12 | struct Cli {
13 |     #[command(subcommand)]
14 |     command: Commands,
15 | }
16 | 
17 | #[derive(Subcommand)]
18 | enum Commands {
19 |     /// Start the Google Drive server
20 |     Drive,
21 |     /// Start the Google Sheets server
22 |     Sheets,
23 |     Refresh {
24 |         /// Google OAuth client ID
25 |         #[arg(long, env = "GOOGLE_CLIENT_ID")]
26 |         client_id: String,
27 |         /// Google OAuth client secret
28 |         #[arg(long, env = "GOOGLE_CLIENT_SECRET")]
29 |         client_secret: String,
30 |         /// Refresh token
31 |         #[arg(long, env = "GOOGLE_REFRESH_TOKEN")]
32 |         refresh_token: String,
33 |     },
34 | }
35 | 
36 | #[tokio::main]
37 | async fn main() -> Result<()> {
38 |     init_logging("debug");
39 | 
40 |     let cli = Cli::parse();
41 | 
42 |     match cli.command {
43 |         Commands::Drive => {
44 |             let server = drive::build(ServerStdioTransport)?;
45 |             let server_handle = tokio::spawn(async move { server.listen().await });
46 | 
47 |             server_handle
48 |                 .await?
49 |                 .map_err(|e| anyhow::anyhow!("Drive server error: {:#?}", e))?;
50 |         }
51 |         Commands::Sheets => {
52 |             let server = sheets::build(ServerStdioTransport)?;
53 |             let server_handle = tokio::spawn(async move { server.listen().await });
54 | 
55 |             server_handle
56 |                 .await?
57 |                 .map_err(|e| anyhow::anyhow!("Sheets server error: {:#?}", e))?;
58 |         }
59 |         Commands::Refresh {
60 |             client_id,
61 |             client_secret,
62 |             refresh_token,
63 |         } => {
64 |             let auth_service = GoogleAuthService::new(client_id, client_secret).unwrap();
65 |             let token_response = auth_service.refresh_token(&refresh_token).await.unwrap();
66 |             println!("Token response: {:#?}", token_response);
67 |         }
68 |     }
69 | 
70 |     Ok(())
71 | }
72 | 
```

--------------------------------------------------------------------------------
/src/auth.rs:
--------------------------------------------------------------------------------

```rust
 1 | use reqwest::Client;
 2 | use serde::{Deserialize, Serialize};
 3 | use serde_json::json;
 4 | use tracing::debug;
 5 | 
 6 | use crate::InvokeError;
 7 | 
 8 | #[derive(Serialize, Deserialize, Debug, Clone)]
 9 | pub struct TokenResponse {
10 |     pub access_token: String,
11 |     pub expires_in: i32,
12 |     pub refresh_token: Option<String>,
13 |     pub scope: String,
14 |     pub token_type: String,
15 | }
16 | 
17 | #[derive(Clone)]
18 | pub struct GoogleAuthService {
19 |     pub client: Client,
20 |     pub google_client_id: String,
21 |     pub google_client_secret: String,
22 | }
23 | impl Default for GoogleAuthService {
24 |     fn default() -> Self {
25 |         let google_client_id = std::env::var("GOOGLE_CLIENT_ID")
26 |             .map_err(|_| InvokeError::EnvVarMissing("GOOGLE_CLIENT_ID".to_string()))
27 |             .unwrap();
28 |         let google_client_secret = std::env::var("GOOGLE_CLIENT_SECRET")
29 |             .map_err(|_| InvokeError::EnvVarMissing("GOOGLE_CLIENT_SECRET".to_string()))
30 |             .unwrap();
31 | 
32 |         Self::new(google_client_id, google_client_secret).unwrap()
33 |     }
34 | }
35 | impl GoogleAuthService {
36 |     pub fn new(client_id: String, client_secret: String) -> Result<Self, InvokeError> {
37 |         Ok(Self {
38 |             client: Client::new(),
39 |             google_client_id: client_id,
40 |             google_client_secret: client_secret,
41 |         })
42 |     }
43 | 
44 |     pub async fn refresh_token(&self, refresh_token: &str) -> Result<TokenResponse, InvokeError> {
45 |         let payload = json!({
46 |             "client_id": self.google_client_id,
47 |             "client_secret": self.google_client_secret,
48 |             "refresh_token": refresh_token,
49 |             "grant_type": "refresh_token"
50 |         });
51 | 
52 |         self.exchange_token(&payload).await
53 |     }
54 | 
55 |     async fn exchange_token(
56 |         &self,
57 |         payload: &serde_json::Value,
58 |     ) -> Result<TokenResponse, InvokeError> {
59 |         debug!("Token exchange payload: {:?}", payload);
60 | 
61 |         let response = self
62 |             .client
63 |             .post("https://oauth2.googleapis.com/token")
64 |             .json(payload)
65 |             .send()
66 |             .await
67 |             .map_err(|e| InvokeError::GoogleApi(e.to_string()))?;
68 | 
69 |         if !response.status().is_success() {
70 |             let error = response
71 |                 .text()
72 |                 .await
73 |                 .unwrap_or_else(|_| "Unknown error".to_string());
74 |             return Err(InvokeError::GoogleApi(error));
75 |         }
76 | 
77 |         response
78 |             .json::<TokenResponse>()
79 |             .await
80 |             .map_err(|e| InvokeError::TokenParse(e.to_string()))
81 |     }
82 | }
83 | 
```

--------------------------------------------------------------------------------
/src/tests/drive.rs:
--------------------------------------------------------------------------------

```rust
 1 | use crate::{client::get_drive_client, logging::init_logging, servers::drive};
 2 | use async_mcp::{
 3 |     protocol::RequestOptions,
 4 |     transport::{ClientInMemoryTransport, ServerInMemoryTransport, Transport},
 5 |     types::CallToolRequest,
 6 | };
 7 | use dotenv::dotenv;
 8 | use serde_json::json;
 9 | use std::{collections::HashMap, env, time::Duration};
10 | 
11 | async fn async_drive_server(transport: ServerInMemoryTransport) {
12 |     let server = drive::build(transport).unwrap();
13 |     server.listen().await.unwrap();
14 | }
15 | 
16 | #[tokio::test]
17 | async fn test_drive_operations() -> anyhow::Result<()> {
18 |     dotenv::dotenv().ok();
19 |     let access_token = env::var("GOOGLE_ACCESS_TOKEN").unwrap();
20 | 
21 |     let client_transport = ClientInMemoryTransport::new(move |t| {
22 |         tokio::spawn(async move { async_drive_server(t).await })
23 |     });
24 |     client_transport.open().await?;
25 | 
26 |     let client = async_mcp::client::ClientBuilder::new(client_transport.clone()).build();
27 |     let client_clone = client.clone();
28 |     let _client_handle = tokio::spawn(async move { client_clone.start().await });
29 | 
30 |     let params = CallToolRequest {
31 |         name: "list_files".to_string(),
32 |         arguments: Some(HashMap::from([
33 |             (
34 |                 "mime_type".to_string(),
35 |                 "application/vnd.google-apps.folder".to_string().into(),
36 |             ),
37 |             ("page_size".to_string(), 5.into()),
38 |         ])),
39 |         meta: Some(json!({
40 |             "access_token": access_token
41 |         })),
42 |     };
43 |     // Test list files
44 |     let response = client
45 |         .request(
46 |             "list_files",
47 |             Some(serde_json::to_value(&params).unwrap()),
48 |             RequestOptions::default().timeout(Duration::from_secs(5)),
49 |         )
50 |         .await?;
51 |     println!("List files response:\n{response}");
52 | 
53 |     Ok(())
54 | }
55 | 
56 | #[tokio::test]
57 | async fn test_list_spreadsheets() -> Result<(), Box<dyn std::error::Error>> {
58 |     init_logging("debug");
59 |     dotenv().ok();
60 | 
61 |     let access_token = env::var("GOOGLE_ACCESS_TOKEN").unwrap();
62 |     let drive = get_drive_client(&access_token);
63 | 
64 |     // Add more detailed query parameters and debug output
65 |     let result = drive
66 |         .files()
67 |         .list()
68 |         .q("mimeType='application/vnd.google-apps.spreadsheet'")
69 |         .order_by("modifiedTime desc")
70 |         .page_size(10) // Limit to 10 results for testing
71 |         .doit()
72 |         .await?;
73 | 
74 |     if let Some(files) = result.1.files {
75 |         for file in files {
76 |             println!(
77 |                 "Spreadsheet: {} (ID: {})",
78 |                 file.name.unwrap_or_default(),
79 |                 file.id.unwrap_or_default()
80 |             );
81 |             println!(
82 |                 "Last modified: {:?}",
83 |                 file.modified_time.unwrap_or_default()
84 |             );
85 |             println!("-------------------");
86 |         }
87 |     }
88 | 
89 |     Ok(())
90 | }
91 | 
```

--------------------------------------------------------------------------------
/src/servers/drive.rs:
--------------------------------------------------------------------------------

```rust
  1 | use anyhow::Result;
  2 | use async_mcp::{
  3 |     server::Server,
  4 |     transport::Transport,
  5 |     types::{
  6 |         CallToolRequest, CallToolResponse, ListRequest, Resource, ResourcesListResponse,
  7 |         ServerCapabilities, Tool, ToolResponseContent,
  8 |     },
  9 | };
 10 | use serde_json::json;
 11 | use url::Url;
 12 | 
 13 | use crate::client::get_drive_client;
 14 | 
 15 | fn get_access_token(req: &CallToolRequest) -> Result<&str> {
 16 |     req.meta
 17 |         .as_ref()
 18 |         .and_then(|v| v.get("access_token"))
 19 |         .and_then(|v| v.as_str())
 20 |         .ok_or_else(|| anyhow::anyhow!("Missing or invalid access_token"))
 21 | }
 22 | 
 23 | pub fn build<T: Transport>(transport: T) -> Result<Server<T>> {
 24 |     let mut server = Server::builder(transport)
 25 |         .capabilities(ServerCapabilities {
 26 |             tools: Some(json!({
 27 |                 "drive": {
 28 |                     "version": "v3",
 29 |                     "description": "Google Drive API operations"
 30 |                 }
 31 |             })),
 32 |             ..Default::default()
 33 |         })
 34 |         .request_handler("resources/list", |_req: ListRequest| {
 35 |             Box::pin(async move { Ok(list_drive_resources()) })
 36 |         });
 37 | 
 38 |     // List files
 39 |     server.register_tool(
 40 |         Tool {
 41 |             name: "list_files".to_string(),
 42 |             description: Some("List files in Google Drive with filters".to_string()),
 43 |             input_schema: json!({
 44 |                 "type": "object",
 45 |                 "properties": {
 46 |                     "mime_type": {"type": "string"},
 47 |                     "query": {"type": "string"},
 48 |                     "page_size": {"type": "integer", "default": 10},
 49 |                     "order_by": {"type": "string", "default": "modifiedTime desc"}
 50 |                 }
 51 |             }),
 52 |         },
 53 |         move |req: CallToolRequest| {
 54 |             Box::pin(async move {
 55 |                 let access_token = get_access_token(&req)?;
 56 |                 let args = req.arguments.clone().unwrap_or_default();
 57 | 
 58 |                 let result = async {
 59 |                     let drive = get_drive_client(access_token);
 60 | 
 61 |                     let mut query = String::new();
 62 |                     if let Some(mime_type) = args.get("mime_type").and_then(|v| v.as_str()) {
 63 |                         query.push_str(&format!("mimeType='{}'", mime_type));
 64 |                     }
 65 | 
 66 |                     let result = drive
 67 |                         .files()
 68 |                         .list()
 69 |                         .q(&query)
 70 |                         .page_size(
 71 |                             args.get("page_size").and_then(|v| v.as_u64()).unwrap_or(10) as i32
 72 |                         )
 73 |                         .order_by(
 74 |                             args.get("order_by")
 75 |                                 .and_then(|v| v.as_str())
 76 |                                 .unwrap_or("modifiedTime desc"),
 77 |                         )
 78 |                         .doit()
 79 |                         .await?;
 80 | 
 81 |                     Ok(CallToolResponse {
 82 |                         content: vec![ToolResponseContent::Text {
 83 |                             text: serde_json::to_string(&result.1)?,
 84 |                         }],
 85 |                         is_error: None,
 86 |                         meta: None,
 87 |                     })
 88 |                 }
 89 |                 .await;
 90 | 
 91 |                 handle_result(result)
 92 |             })
 93 |         },
 94 |     );
 95 | 
 96 |     Ok(server.build())
 97 | }
 98 | 
 99 | fn list_drive_resources() -> ResourcesListResponse {
100 |     let base = Url::parse("https://www.googleapis.com/drive/v3/").unwrap();
101 |     ResourcesListResponse {
102 |         resources: vec![Resource {
103 |             uri: base,
104 |             name: "drive".to_string(),
105 |             description: Some("Google Drive API".to_string()),
106 |             mime_type: Some("application/json".to_string()),
107 |         }],
108 |         next_cursor: None,
109 |         meta: None,
110 |     }
111 | }
112 | 
113 | fn handle_result(result: Result<CallToolResponse>) -> Result<CallToolResponse> {
114 |     match result {
115 |         Ok(response) => Ok(response),
116 |         Err(e) => Ok(CallToolResponse {
117 |             content: vec![ToolResponseContent::Text {
118 |                 text: format!("Error: {}", e),
119 |             }],
120 |             is_error: Some(true),
121 |             meta: None,
122 |         }),
123 |     }
124 | }
125 | 
```

--------------------------------------------------------------------------------
/src/tests/sheets.rs:
--------------------------------------------------------------------------------

```rust
  1 | use crate::{
  2 |     client::{get_drive_client, get_sheets_client},
  3 |     servers::sheets,
  4 | };
  5 | use async_mcp::{
  6 |     protocol::RequestOptions,
  7 |     transport::{ClientInMemoryTransport, ServerInMemoryTransport, Transport},
  8 |     types::CallToolRequest,
  9 | };
 10 | use dotenv::dotenv;
 11 | use serde::{Deserialize, Serialize};
 12 | use serde_json::json;
 13 | use std::{collections::HashMap, env, time::Duration};
 14 | 
 15 | #[derive(Debug, Serialize, Deserialize)]
 16 | #[serde(rename_all = "camelCase")]
 17 | struct Sheet {
 18 |     a1_notation: String,
 19 |     sheet_id: u64,
 20 |     sheet_name: String,
 21 | }
 22 | 
 23 | async fn async_sheets_server(transport: ServerInMemoryTransport) {
 24 |     println!("Starting sheets server...");
 25 |     let server = sheets::build(transport).unwrap();
 26 |     println!("Server built successfully");
 27 |     server.listen().await.unwrap();
 28 | }
 29 | 
 30 | #[tokio::test]
 31 | async fn test_sheets_operations() -> anyhow::Result<()> {
 32 |     dotenv::dotenv().ok();
 33 |     let access_token = env::var("GOOGLE_ACCESS_TOKEN").unwrap();
 34 |     let spreadsheet_id = env::var("TEST_SPREADSHEET_ID").unwrap();
 35 | 
 36 |     let client_transport = ClientInMemoryTransport::new(move |t| {
 37 |         tokio::spawn(async move { async_sheets_server(t).await })
 38 |     });
 39 |     client_transport.open().await?;
 40 | 
 41 |     let client = async_mcp::client::ClientBuilder::new(client_transport.clone()).build();
 42 |     let client_clone = client.clone();
 43 |     let _client_handle = tokio::spawn(async move { client_clone.start().await });
 44 | 
 45 |     // Add a small delay to ensure server is ready
 46 |     tokio::time::sleep(std::time::Duration::from_millis(100)).await;
 47 | 
 48 |     let params = CallToolRequest {
 49 |         name: "read_values".to_string(),
 50 |         arguments: Some(HashMap::new()),
 51 |         meta: Some(json!({
 52 |             "access_token": access_token,
 53 |             "spreadsheet_id": spreadsheet_id,
 54 |             "sheet": "Sheet6"
 55 |         })),
 56 |     };
 57 | 
 58 |     // Test read values
 59 |     let response = client
 60 |         .request(
 61 |             "tools/call",
 62 |             Some(serde_json::to_value(&params).unwrap()),
 63 |             RequestOptions::default().timeout(Duration::from_secs(5)),
 64 |         )
 65 |         .await?;
 66 | 
 67 |     // Add better error handling
 68 |     let response_obj: serde_json::Value = serde_json::from_str(&response.to_string())?;
 69 |     if let Some(error) = response_obj.get("error") {
 70 |         println!("Error reading sheet: {}", error);
 71 |         anyhow::bail!("Failed to read sheet: {}", error);
 72 |     }
 73 | 
 74 |     println!("Read values response:\n{response}");
 75 | 
 76 |     Ok(())
 77 | }
 78 | 
 79 | #[tokio::test]
 80 | async fn test_google_sheets() -> Result<(), Box<dyn std::error::Error>> {
 81 |     dotenv().ok();
 82 |     // let access_token = env::var("GOOGLE_ACCESS_TOKEN").unwrap();
 83 |     // let auth_service = GoogleAuthService::new(
 84 |     //     env::var("GOOGLE_CLIENT_ID").unwrap(),
 85 |     //     env::var("GOOGLE_CLIENT_SECRET").unwrap(),
 86 |     // )?;
 87 |     // let token_response = auth_service.refresh_token(&access_token).await?;
 88 |     // println!("Access token: {:?}", token_response);
 89 |     // let access_token = token_response.access_token;
 90 | 
 91 |     let access_token = env::var("GOOGLE_ACCESS_TOKEN").unwrap();
 92 |     let sheets = get_sheets_client(&access_token);
 93 | 
 94 |     let spreadsheet_id = env::var("TEST_SPREADSHEET_ID").unwrap();
 95 | 
 96 |     // Try to read the spreadsheet
 97 |     let result = sheets.spreadsheets().get(&spreadsheet_id).doit().await?;
 98 |     // Extract sheet names and ranges
 99 |     if let Some(sheets) = result.1.sheets {
100 |         for sheet in sheets {
101 |             if let Some(properties) = sheet.properties {
102 |                 let sheet_title = properties.title.unwrap_or_default();
103 |                 let grid_props = properties.grid_properties.unwrap_or_default();
104 |                 let row_count = grid_props.row_count.unwrap_or(0);
105 |                 let column_count = grid_props.column_count.unwrap_or(0);
106 | 
107 |                 println!(
108 |                     "Sheet: {}\nRange: {}!A1:{}{}\n",
109 |                     sheet_title,
110 |                     sheet_title,
111 |                     (b'A' + (column_count as u8) - 1) as char,
112 |                     row_count
113 |                 );
114 |             }
115 |         }
116 |     } else {
117 |         println!("No sheets found.");
118 |     }
119 | 
120 |     Ok(())
121 | }
122 | 
123 | #[tokio::test]
124 | async fn test_list_spreadsheet_details() -> Result<(), Box<dyn std::error::Error>> {
125 |     dotenv().ok();
126 |     let access_token = env::var("GOOGLE_ACCESS_TOKEN").unwrap();
127 | 
128 |     let drive = get_drive_client(&access_token);
129 |     let sheets = get_sheets_client(&access_token);
130 | 
131 |     let result = drive
132 |         .files()
133 |         .list()
134 |         .q("mimeType='application/vnd.google-apps.spreadsheet'")
135 |         .order_by("modifiedTime desc")
136 |         .page_size(10)
137 |         .doit()
138 |         .await?;
139 | 
140 |     if let Some(files) = result.1.files {
141 |         for file in files {
142 |             let id = file.id.clone().unwrap_or_default();
143 |             println!(
144 |                 "Spreadsheet: {} (ID: {})",
145 |                 file.name.unwrap_or_default(),
146 |                 id
147 |             );
148 | 
149 |             // Get the content of each spreadsheet
150 |             let spreadsheet = sheets.spreadsheets().get(&id).doit().await?;
151 | 
152 |             println!("Sheets in this spreadsheet:");
153 |             for sheet in spreadsheet.1.sheets.unwrap_or_default() {
154 |                 if let Some(props) = sheet.properties {
155 |                     println!("- Sheet name: {}", props.title.unwrap_or_default());
156 |                 }
157 |             }
158 |             println!("-------------------");
159 |         }
160 |     }
161 | 
162 |     Ok(())
163 | }
164 | 
165 | #[tokio::test]
166 | async fn test_sheet_operations() -> anyhow::Result<()> {
167 |     dotenv().ok();
168 |     let access_token = env::var("GOOGLE_ACCESS_TOKEN").unwrap();
169 |     let spreadsheet_id = env::var("TEST_SPREADSHEET_ID").unwrap();
170 | 
171 |     let client_transport = ClientInMemoryTransport::new(move |t| {
172 |         tokio::spawn(async move { async_sheets_server(t).await })
173 |     });
174 |     client_transport.open().await?;
175 | 
176 |     let client = async_mcp::client::ClientBuilder::new(client_transport.clone()).build();
177 |     let client_clone = client.clone();
178 |     let _client_handle = tokio::spawn(async move { client_clone.start().await });
179 | 
180 |     // Add a small delay to ensure server is ready
181 |     tokio::time::sleep(Duration::from_millis(100)).await;
182 | 
183 |     // First get sheet info
184 |     let get_info_params = CallToolRequest {
185 |         name: "get_sheet_info".to_string(),
186 |         arguments: None,
187 |         meta: Some(json!({
188 |             "access_token": access_token,
189 |             "spreadsheet_id": spreadsheet_id,
190 |         })),
191 |     };
192 | 
193 |     let info_response = client
194 |         .request(
195 |             "tools/call",
196 |             Some(serde_json::to_value(&get_info_params).unwrap()),
197 |             RequestOptions::default().timeout(Duration::from_secs(5)),
198 |         )
199 |         .await?;
200 | 
201 |     println!("Sheet info:\n{}", info_response);
202 | 
203 |     // Read the current value from A1
204 |     let read_params = CallToolRequest {
205 |         name: "read_values".to_string(),
206 |         arguments: None,
207 |         meta: Some(json!({
208 |             "access_token": access_token,
209 |             "spreadsheet_id": spreadsheet_id,
210 |             "sheet": "Sheet1",
211 |             "range": "A1"
212 |         })),
213 |     };
214 | 
215 |     let read_response = client
216 |         .request(
217 |             "tools/call",
218 |             Some(serde_json::to_value(&read_params).unwrap()),
219 |             RequestOptions::default().timeout(Duration::from_secs(5)),
220 |         )
221 |         .await?;
222 | 
223 |     // After read_response
224 |     println!("Initial read response:\n{}", read_response);
225 | 
226 |     // Parse the current value and increment it
227 |     let read_value = serde_json::from_str::<serde_json::Value>(&read_response.to_string())?;
228 |     println!("Parsed read value: {:?}", read_value);
229 | 
230 |     let current_value = read_value["content"][0]["text"]
231 |         .as_str()
232 |         .and_then(|s| serde_json::from_str::<serde_json::Value>(s).ok())
233 |         .and_then(|v| v["values"][0][0].as_str().map(String::from))
234 |         .and_then(|s| s.parse::<i32>().ok())
235 |         .unwrap_or(0);
236 |     let new_value = current_value + 1;
237 | 
238 |     // Write the incremented value back
239 |     let mut args = HashMap::new();
240 |     args.insert("values".to_string(), json!([[new_value.to_string()]]));
241 |     args.insert("range".to_string(), json!("A1"));
242 | 
243 |     let write_params = CallToolRequest {
244 |         name: "write_values".to_string(),
245 |         arguments: Some(args),
246 |         meta: Some(json!({
247 |             "access_token": access_token,
248 |             "spreadsheet_id": spreadsheet_id,
249 |             "sheet": "Sheet1"
250 |         })),
251 |     };
252 | 
253 |     let write_response = client
254 |         .request(
255 |             "tools/call",
256 |             Some(serde_json::to_value(&write_params).unwrap()),
257 |             RequestOptions::default().timeout(Duration::from_secs(5)),
258 |         )
259 |         .await?;
260 | 
261 |     println!("Write response:\n{}", write_response);
262 | 
263 |     // Verify the new value
264 |     let verify_response = client
265 |         .request(
266 |             "tools/call",
267 |             Some(serde_json::to_value(&read_params).unwrap()),
268 |             RequestOptions::default().timeout(Duration::from_secs(5)),
269 |         )
270 |         .await?;
271 | 
272 |     // After verify_response
273 |     println!("Verify response:\n{}", verify_response);
274 | 
275 |     let verify_value = serde_json::from_str::<serde_json::Value>(&verify_response.to_string())?;
276 |     println!("Parsed verify value: {:?}", verify_value);
277 | 
278 |     let updated_value = verify_value["content"][0]["text"]
279 |         .as_str()
280 |         .and_then(|s| serde_json::from_str::<serde_json::Value>(s).ok())
281 |         .and_then(|v| v["values"][0][0].as_str().map(String::from))
282 |         .and_then(|s| s.parse::<i32>().ok())
283 |         .unwrap_or(0);
284 | 
285 |     assert_eq!(
286 |         updated_value, new_value,
287 |         "Value was not updated correctly. Expected {}, got {}",
288 |         new_value, updated_value
289 |     );
290 | 
291 |     println!(
292 |         "Successfully incremented value from {} to {}",
293 |         current_value, new_value
294 |     );
295 | 
296 |     Ok(())
297 | }
298 | 
```

--------------------------------------------------------------------------------
/src/servers/sheets.rs:
--------------------------------------------------------------------------------

```rust
  1 | use anyhow::{Context, Result};
  2 | use async_mcp::{
  3 |     server::{Server, ServerBuilder},
  4 |     transport::Transport,
  5 |     types::{
  6 |         CallToolRequest, CallToolResponse, ListRequest, Resource, ResourcesListResponse,
  7 |         ServerCapabilities, Tool, ToolResponseContent,
  8 |     },
  9 | };
 10 | use serde_json::json;
 11 | use url::Url;
 12 | 
 13 | use crate::client::get_sheets_client;
 14 | 
 15 | fn get_access_token(req: &CallToolRequest) -> Result<&str> {
 16 |     req.meta
 17 |         .as_ref()
 18 |         .and_then(|v| v.get("access_token"))
 19 |         .and_then(|v| v.as_str())
 20 |         .ok_or_else(|| anyhow::anyhow!("Missing or invalid access_token"))
 21 | }
 22 | 
 23 | pub fn build<T: Transport>(transport: T) -> Result<Server<T>> {
 24 |     let mut server = Server::builder(transport)
 25 |         .capabilities(ServerCapabilities {
 26 |             tools: Some(json!({
 27 |                 "sheets": {
 28 |                     "version": "v4",
 29 |                     "description": "Google Sheets API operations"
 30 |                 }
 31 |             })),
 32 |             ..Default::default()
 33 |         })
 34 |         .request_handler("resources/list", |_req: ListRequest| {
 35 |             Box::pin(async move { Ok(list_sheets_resources()) })
 36 |         });
 37 | 
 38 |     register_tools(&mut server)?;
 39 | 
 40 |     Ok(server.build())
 41 | }
 42 | 
 43 | fn register_tools<T: Transport>(server: &mut ServerBuilder<T>) -> Result<()> {
 44 |     // Tool Definitions
 45 |     let read_values_tool = Tool {
 46 |         name: "read_values".to_string(),
 47 |         description: Some("Read values from a Google Sheet".to_string()),
 48 |         input_schema: json!({
 49 |             "type": "object",
 50 |             "properties": {
 51 |                 "sheet": {"type": "string", "description": "Sheet name"},
 52 |                 "range": {"type": "string", "description": "Range to read (e.g. 'A1:B2')", "default": "A1:ZZ"},
 53 |                 "major_dimension": {"type": "string", "enum": ["ROWS", "COLUMNS"], "default": "ROWS"}
 54 |             },
 55 |             "required": ["sheet"]
 56 |         }),
 57 |     };
 58 | 
 59 |     let write_values_tool = Tool {
 60 |         name: "write_values".to_string(),
 61 |         description: Some("Write values to a Google Sheet".to_string()),
 62 |         input_schema: json!({
 63 |             "type": "object",
 64 |             "properties": {
 65 |                 "sheet": {"type": "string", "description": "Sheet name"},
 66 |                 "range": {"type": "string", "description": "Range to write to (e.g. 'A1:B2')"},
 67 |                 "values": {
 68 |                     "description": "2D array of values to write",
 69 |                     "type": "array",
 70 |                     "items": {
 71 |                         "type": "array",
 72 |                         "items": {
 73 |                         "type": ["string", "number", "boolean", "null"],
 74 |                         "description": "A single cell value"
 75 |                         }
 76 |                     }
 77 |                 },
 78 |                 "major_dimension": {"type": "string", "enum": ["ROWS", "COLUMNS"], "default": "ROWS"}
 79 |             },
 80 |             "required": ["values", "range", "sheet"]
 81 |         }),
 82 |     };
 83 | 
 84 |     let create_spreadsheet_tool = Tool {
 85 |         name: "create_spreadsheet".to_string(),
 86 |         description: Some("Create a new Google Sheet".to_string()),
 87 |         input_schema: json!({
 88 |             "type": "object",
 89 |             "properties": {
 90 |                 "title": {"type": "string"},
 91 |                 "sheets": {
 92 |                     "type": "array",
 93 |                     "items": {
 94 |                         "type": "object",
 95 |                         "properties": {
 96 |                             "title": {"type": "string"}
 97 |                         }
 98 |                     }
 99 |                 }
100 |             },
101 |             "required": ["title"]
102 |         }),
103 |     };
104 | 
105 |     let clear_values_tool = Tool {
106 |         name: "clear_values".to_string(),
107 |         description: Some("Clear values from a range in a Google Sheet".to_string()),
108 |         input_schema: json!({
109 |             "type": "object",
110 |             "properties": {
111 |                 "sheet": {"type": "string", "description": "Sheet name", "default": "Sheet1"},
112 |                 "range": {"type": "string", "description": "Range to clear (e.g. 'A1:B2')", "default": "A1:ZZ"}
113 |             },
114 |             "required": ["sheet", "range"]
115 |         }),
116 |     };
117 | 
118 |     let get_sheet_info_tool = Tool {
119 |         name: "get_sheet_info".to_string(),
120 |         description: Some("Get information about all sheets in a spreadsheet, including their titles and maximum ranges (e.g. 'A1:Z1000'). This is useful for discovering what sheets exist and their dimensions.".to_string()),
121 |         input_schema: json!({
122 |             "type": "object",
123 |             "properties": {},
124 |             "required": []
125 |         }),
126 |     };
127 | 
128 |     // Tool Implementations
129 |     server.register_tool(read_values_tool, move |req: CallToolRequest| {
130 |         Box::pin(async move {
131 |             let access_token = get_access_token(&req)?;
132 |             let args = req.arguments.clone().unwrap_or_default();
133 |             let context = req.meta.clone().unwrap_or_default();
134 | 
135 |             let result = async {
136 |                 let sheets = get_sheets_client(access_token);
137 | 
138 |                 let spreadsheet_id = context
139 |                     .get("spreadsheet_id")
140 |                     .and_then(|v| v.as_str())
141 |                     .context("spreadsheet_id required in context")?;
142 | 
143 |                 let sheet = args["sheet"].as_str().context("sheet name required")?;
144 |                 let user_range = args["range"].as_str().unwrap_or("A1:ZZ");
145 |                 let range = format!("{}!{}", sheet, user_range);
146 | 
147 |                 let major_dimension = args
148 |                     .get("major_dimension")
149 |                     .and_then(|v| v.as_str())
150 |                     .unwrap_or("ROWS");
151 | 
152 |                 let result = sheets
153 |                     .spreadsheets()
154 |                     .values_get(spreadsheet_id, &range)
155 |                     .major_dimension(major_dimension)
156 |                     .doit()
157 |                     .await?;
158 | 
159 |                 Ok(CallToolResponse {
160 |                     content: vec![ToolResponseContent::Text {
161 |                         text: serde_json::to_string(&result.1)?,
162 |                     }],
163 |                     is_error: None,
164 |                     meta: None,
165 |                 })
166 |             }
167 |             .await;
168 | 
169 |             handle_result(result)
170 |         })
171 |     });
172 | 
173 |     server.register_tool(write_values_tool, move |req: CallToolRequest| {
174 |         Box::pin(async move {
175 |             let access_token = get_access_token(&req)?;
176 |             let args = req.arguments.clone().unwrap_or_default();
177 |             let context = req.meta.clone().unwrap_or_default();
178 | 
179 |             let result = async {
180 |                 let sheets = get_sheets_client(access_token);
181 | 
182 |                 let spreadsheet_id = context
183 |                     .get("spreadsheet_id")
184 |                     .and_then(|v| v.as_str())
185 |                     .context("spreadsheet_id required in context")?;
186 | 
187 |                 let sheet = args["sheet"].as_str().context("sheet name required")?;
188 |                 let user_range = args["range"].as_str().context("range is required")?;
189 |                 let range = format!("{}!{}", sheet, user_range);
190 | 
191 |                 let values = args
192 |                     .get("values")
193 |                     .and_then(|v| v.as_array())
194 |                     .context("values required")?;
195 |                 let major_dimension = args
196 |                     .get("major_dimension")
197 |                     .and_then(|v| v.as_str())
198 |                     .unwrap_or("ROWS");
199 | 
200 |                 let mut value_range = google_sheets4::api::ValueRange::default();
201 |                 value_range.major_dimension = Some(major_dimension.to_string());
202 |                 value_range.values = Some(
203 |                     values
204 |                         .iter()
205 |                         .map(|row| {
206 |                             row.as_array()
207 |                                 .unwrap_or(&vec![])
208 |                                 .iter()
209 |                                 .map(|v| v.as_str().unwrap_or_default().to_string().into())
210 |                                 .collect::<Vec<serde_json::Value>>()
211 |                         })
212 |                         .collect(),
213 |                 );
214 | 
215 |                 let result = sheets
216 |                     .spreadsheets()
217 |                     .values_update(value_range, spreadsheet_id, &range)
218 |                     .value_input_option("RAW")
219 |                     .doit()
220 |                     .await?;
221 | 
222 |                 Ok(CallToolResponse {
223 |                     content: vec![ToolResponseContent::Text {
224 |                         text: serde_json::to_string(&result.1)?,
225 |                     }],
226 |                     is_error: None,
227 |                     meta: None,
228 |                 })
229 |             }
230 |             .await;
231 | 
232 |             handle_result(result)
233 |         })
234 |     });
235 | 
236 |     server.register_tool(create_spreadsheet_tool, move |req: CallToolRequest| {
237 |         Box::pin(async move {
238 |             let access_token = get_access_token(&req)?;
239 |             let args = req.arguments.clone().unwrap_or_default();
240 |             let result = async {
241 |                 let sheets = get_sheets_client(access_token);
242 | 
243 |                 let title = args["title"].as_str().context("title required")?;
244 | 
245 |                 let mut spreadsheet = google_sheets4::api::Spreadsheet::default();
246 |                 spreadsheet.properties = Some(google_sheets4::api::SpreadsheetProperties {
247 |                     title: Some(title.to_string()),
248 |                     ..Default::default()
249 |                 });
250 | 
251 |                 // Add sheets if specified
252 |                 if let Some(sheet_configs) = args["sheets"].as_array() {
253 |                     let sheets = sheet_configs
254 |                         .iter()
255 |                         .map(|config| {
256 |                             let title = config["title"].as_str().unwrap_or("Sheet1").to_string();
257 |                             google_sheets4::api::Sheet {
258 |                                 properties: Some(google_sheets4::api::SheetProperties {
259 |                                     title: Some(title),
260 |                                     ..Default::default()
261 |                                 }),
262 |                                 ..Default::default()
263 |                             }
264 |                         })
265 |                         .collect();
266 |                     spreadsheet.sheets = Some(sheets);
267 |                 }
268 | 
269 |                 let result = sheets.spreadsheets().create(spreadsheet).doit().await?;
270 | 
271 |                 Ok(CallToolResponse {
272 |                     content: vec![ToolResponseContent::Text {
273 |                         text: serde_json::to_string(&result.1)?,
274 |                     }],
275 |                     is_error: None,
276 |                     meta: None,
277 |                 })
278 |             }
279 |             .await;
280 | 
281 |             handle_result(result)
282 |         })
283 |     });
284 | 
285 |     server.register_tool(clear_values_tool, move |req: CallToolRequest| {
286 |         Box::pin(async move {
287 |             let access_token = get_access_token(&req)?;
288 |             let args = req.arguments.clone().unwrap_or_default();
289 |             let context = req.meta.clone().unwrap_or_default();
290 | 
291 |             let result = async {
292 |                 let sheets = get_sheets_client(access_token);
293 | 
294 |                 let spreadsheet_id = context
295 |                     .get("spreadsheet_id")
296 |                     .and_then(|v| v.as_str())
297 |                     .context("spreadsheet_id required in context")?;
298 | 
299 |                 let sheet = args
300 |                     .get("sheet")
301 |                     .and_then(|v| v.as_str())
302 |                     .unwrap_or("Sheet1");
303 |                 let user_range = args
304 |                     .get("range")
305 |                     .and_then(|v| v.as_str())
306 |                     .unwrap_or("A1:ZZ");
307 |                 let range = format!("{}!{}", sheet, user_range);
308 | 
309 |                 let clear_request = google_sheets4::api::ClearValuesRequest::default();
310 |                 let result = sheets
311 |                     .spreadsheets()
312 |                     .values_clear(clear_request, spreadsheet_id, &range)
313 |                     .doit()
314 |                     .await?;
315 | 
316 |                 Ok(CallToolResponse {
317 |                     content: vec![ToolResponseContent::Text {
318 |                         text: serde_json::to_string(&result.1)?,
319 |                     }],
320 |                     is_error: None,
321 |                     meta: None,
322 |                 })
323 |             }
324 |             .await;
325 | 
326 |             handle_result(result)
327 |         })
328 |     });
329 | 
330 |     server.register_tool(get_sheet_info_tool, move |req: CallToolRequest| {
331 |         Box::pin(async move {
332 |             let access_token = get_access_token(&req)?;
333 |             let context = req.meta.clone().unwrap_or_default();
334 | 
335 |             let result = async {
336 |                 let sheets = get_sheets_client(access_token);
337 | 
338 |                 let spreadsheet_id = context
339 |                     .get("spreadsheet_id")
340 |                     .and_then(|v| v.as_str())
341 |                     .context("spreadsheet_id required in context")?;
342 | 
343 |                 let result = sheets.spreadsheets().get(spreadsheet_id).doit().await?;
344 | 
345 |                 let spreadsheet = result.1;
346 | 
347 |                 // Extract sheet information
348 |                 let sheet_info = spreadsheet
349 |                     .sheets
350 |                     .unwrap_or_default()
351 |                     .into_iter()
352 |                     .filter_map(|sheet| {
353 |                         let props = sheet.properties?;
354 |                         let title = props.title?;
355 |                         let grid_props = props.grid_properties?;
356 | 
357 |                         // Calculate the maximum range based on grid properties
358 |                         let max_col = grid_props.column_count.unwrap_or(26) as u8;
359 |                         let max_row = grid_props.row_count.unwrap_or(1000);
360 |                         let max_range = format!("A1:{}{}", (b'A' + max_col - 1) as char, max_row);
361 | 
362 |                         Some(serde_json::json!({
363 |                             "title": title,
364 |                             "maxRange": max_range,
365 |                         }))
366 |                     })
367 |                     .collect::<Vec<_>>();
368 | 
369 |                 Ok(CallToolResponse {
370 |                     content: vec![ToolResponseContent::Text {
371 |                         text: serde_json::to_string(&sheet_info)?,
372 |                     }],
373 |                     is_error: None,
374 |                     meta: None,
375 |                 })
376 |             }
377 |             .await;
378 | 
379 |             handle_result(result)
380 |         })
381 |     });
382 | 
383 |     Ok(())
384 | }
385 | 
386 | fn list_sheets_resources() -> ResourcesListResponse {
387 |     let base = Url::parse("https://sheets.googleapis.com/v4/").unwrap();
388 |     ResourcesListResponse {
389 |         resources: vec![Resource {
390 |             uri: base,
391 |             name: "sheets".to_string(),
392 |             description: Some("Google Sheets API".to_string()),
393 |             mime_type: Some("application/json".to_string()),
394 |         }],
395 |         next_cursor: None,
396 |         meta: None,
397 |     }
398 | }
399 | 
400 | fn handle_result(result: Result<CallToolResponse>) -> Result<CallToolResponse> {
401 |     match result {
402 |         Ok(response) => Ok(response),
403 |         Err(e) => Ok(CallToolResponse {
404 |             content: vec![ToolResponseContent::Text {
405 |                 text: format!("Error: {}", e),
406 |             }],
407 |             is_error: Some(true),
408 |             meta: None,
409 |         }),
410 |     }
411 | }
412 | 
```