diff --git a/Cargo.lock b/Cargo.lock index 6813180c075f..af2c1368c0da 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2543,89 +2543,6 @@ dependencies = [ "regex-syntax 0.8.5", ] -[[package]] -name = "google-apis-common" -version = "7.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7530ee92a7e9247c3294ae1b84ea98474dbc27563c49a14d3938e816499bf38f" -dependencies = [ - "base64 0.22.1", - "chrono", - "http 1.2.0", - "http-body-util", - "hyper 1.6.0", - "hyper-util", - "itertools 0.13.0", - "mime", - "percent-encoding", - "serde", - "serde_json", - "serde_with", - "tokio", - "url", - "yup-oauth2", -] - -[[package]] -name = "google-docs1" -version = "6.0.0+20240613" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8441d3fa1544efacb0fabf88c45ba60d424d718bb13f2a0ce2a6447efb99d14e" -dependencies = [ - "chrono", - "google-apis-common", - "hyper 1.6.0", - "hyper-rustls 0.27.5", - "hyper-util", - "mime", - "serde", - "serde_json", - "serde_with", - "tokio", - "url", - "yup-oauth2", -] - -[[package]] -name = "google-drive3" -version = "6.0.0+20240618" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84e3944ee656d220932785cf1d8275519c0989830b9b239453983ac44f328d9f" -dependencies = [ - "chrono", - "google-apis-common", - "hyper 1.6.0", - "hyper-rustls 0.27.5", - "hyper-util", - "mime", - "serde", - "serde_json", - "serde_with", - "tokio", - "url", - "yup-oauth2", -] - -[[package]] -name = "google-sheets4" -version = "6.0.0+20240621" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4f8ccfc6418e81d1e2ed66fad49d0487526281505b8a0ed8ee770dc7d6bb1e5" -dependencies = [ - "chrono", - "google-apis-common", - "hyper 1.6.0", - "hyper-rustls 0.27.5", - "hyper-util", - "mime", - "serde", - "serde_json", - "serde_with", - "tokio", - "url", - "yup-oauth2", -] - [[package]] name = "goose" version = "1.4.0" @@ -2785,10 +2702,6 @@ dependencies = [ "docx-rs", "etcetera", "glob", - "google-apis-common", - "google-docs1", - "google-drive3", - "google-sheets4", "http-body-util", "hyper 1.6.0", "ignore", @@ -4389,15 +4302,6 @@ dependencies = [ "libc", ] -[[package]] -name = "num_threads" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9" -dependencies = [ - "libc", -] - [[package]] name = "number_prefix" version = "0.4.0" @@ -5814,12 +5718,6 @@ version = "3.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b07779b9b918cc05650cb30f404d4d7835d26df37c235eded8a6832e2fb82cca" -[[package]] -name = "seahash" -version = "4.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" - [[package]] name = "security-framework" version = "2.11.1" @@ -6523,9 +6421,7 @@ checksum = "bb041120f25f8fbe8fd2dbe4671c7c2ed74d83be2e7a77529bf7e0790ae3f472" dependencies = [ "deranged", "itoa", - "libc", "num-conv", - "num_threads", "powerfmt", "serde", "time-core", @@ -8083,33 +7979,6 @@ dependencies = [ "synstructure", ] -[[package]] -name = "yup-oauth2" -version = "11.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ed5f19242090128c5809f6535cc7b8d4e2c32433f6c6005800bbc20a644a7f0" -dependencies = [ - "anyhow", - "async-trait", - "base64 0.22.1", - "futures", - "http 1.2.0", - "http-body-util", - "hyper 1.6.0", - "hyper-rustls 0.27.5", - "hyper-util", - "log", - "percent-encoding", - "rustls 0.23.23", - "rustls-pemfile 2.2.0", - "seahash", - "serde", - "serde_json", - "time", - "tokio", - "url", -] - [[package]] name = "zerocopy" version = "0.7.35" diff --git a/clippy-baselines/too_many_lines.txt b/clippy-baselines/too_many_lines.txt index 13142c9221f2..9f2d0e5d10e4 100644 --- a/clippy-baselines/too_many_lines.txt +++ b/clippy-baselines/too_many_lines.txt @@ -20,14 +20,6 @@ crates/goose-mcp/src/computercontroller/mod.rs::xlsx_tool crates/goose-mcp/src/computercontroller/pdf_tool.rs::pdf_tool crates/goose-mcp/src/developer/mod.rs::bash crates/goose-mcp/src/developer/mod.rs::new -crates/goose-mcp/src/google_drive/google_labels.rs::doit -crates/goose-mcp/src/google_drive/mod.rs::create_file -crates/goose-mcp/src/google_drive/mod.rs::docs_tool -crates/goose-mcp/src/google_drive/mod.rs::new -crates/goose-mcp/src/google_drive/mod.rs::search_files -crates/goose-mcp/src/google_drive/mod.rs::sharing -crates/goose-mcp/src/google_drive/mod.rs::sheets_tool -crates/goose-mcp/src/google_drive/mod.rs::update_label crates/goose-mcp/src/memory/mod.rs::new crates/goose-server/src/openapi.rs::convert_typed_schema crates/goose-server/src/openapi.rs::convert_typed_schema diff --git a/crates/goose-cli/src/cli.rs b/crates/goose-cli/src/cli.rs index ad0bd828ef69..a366f253c710 100644 --- a/crates/goose-cli/src/cli.rs +++ b/crates/goose-cli/src/cli.rs @@ -721,7 +721,7 @@ pub async fn cli() -> Result<()> { return Ok(()); } Some(Command::Mcp { name }) => { - let _ = run_server(&name).await; + run_server(&name).await?; } Some(Command::Session { command, diff --git a/crates/goose-cli/src/commands/configure.rs b/crates/goose-cli/src/commands/configure.rs index d07f969770c5..328ed8e4e56c 100644 --- a/crates/goose-cli/src/commands/configure.rs +++ b/crates/goose-cli/src/commands/configure.rs @@ -31,7 +31,6 @@ fn get_display_name(extension_id: &str) -> String { match extension_id { "developer" => "Developer Tools".to_string(), "computercontroller" => "Computer Controller".to_string(), - "googledrive" => "Google Drive".to_string(), "memory" => "Memory".to_string(), "tutorial" => "Tutorial".to_string(), "jetbrains" => "JetBrains".to_string(), @@ -735,11 +734,6 @@ pub fn configure_extensions_dialog() -> Result<(), Box> { "Developer Tools", "Code editing and shell access", ) - .item( - "googledrive", - "Google Drive", - "Search and read content from google drive - additional config required", - ) .item("jetbrains", "JetBrains", "Connect to jetbrains IDEs") .item( "memory", diff --git a/crates/goose-cli/src/commands/mcp.rs b/crates/goose-cli/src/commands/mcp.rs index f70bd8c6f2ee..6d6b953787d1 100644 --- a/crates/goose-cli/src/commands/mcp.rs +++ b/crates/goose-cli/src/commands/mcp.rs @@ -1,7 +1,5 @@ -use anyhow::Result; -use goose_mcp::{ - ComputerControllerRouter, DeveloperRouter, GoogleDriveRouter, MemoryRouter, TutorialRouter, -}; +use anyhow::{anyhow, Result}; +use goose_mcp::{ComputerControllerRouter, DeveloperRouter, MemoryRouter, TutorialRouter}; use mcp_server::router::RouterService; use mcp_server::{BoundedService, ByteTransport, Server}; use tokio::io::{stdin, stdout}; @@ -17,34 +15,32 @@ use nix::unistd::getpgrp; use nix::unistd::Pid; pub async fn run_server(name: &str) -> Result<()> { - // Initialize logging crate::logging::setup_logging(Some(&format!("mcp-{name}")), None)?; + if name == "googledrive" || name == "google_drive" { + return Err(anyhow!( + "the built-in Google Drive extension has been removed" + )); + } + tracing::info!("Starting MCP server"); let router: Option> = match name { "developer" => Some(Box::new(RouterService(DeveloperRouter::new()))), "computercontroller" => Some(Box::new(RouterService(ComputerControllerRouter::new()))), - "google_drive" | "googledrive" => { - let router = GoogleDriveRouter::new().await; - Some(Box::new(RouterService(router))) - } "memory" => Some(Box::new(RouterService(MemoryRouter::new()))), "tutorial" => Some(Box::new(RouterService(TutorialRouter::new()))), _ => None, }; - // Create shutdown notification channel let shutdown = Arc::new(Notify::new()); let shutdown_clone = shutdown.clone(); - // Spawn shutdown signal handler tokio::spawn(async move { crate::signal::shutdown_signal().await; shutdown_clone.notify_one(); }); - // Create and run the server let server = Server::new(router.unwrap_or_else(|| panic!("Unknown server requested {}", name))); let transport = ByteTransport::new(stdin(), stdout()); diff --git a/crates/goose-mcp/Cargo.toml b/crates/goose-mcp/Cargo.toml index e469b952ea3e..dbf8000483c3 100644 --- a/crates/goose-mcp/Cargo.toml +++ b/crates/goose-mcp/Cargo.toml @@ -38,10 +38,6 @@ chrono = { version = "0.4.38", features = ["serde"] } etcetera = "0.8.0" tempfile = "3.8" include_dir = "0.7.4" -google-apis-common = "7.0.0" -google-drive3 = "6.0.0" -google-sheets4 = "6.0.0" -google-docs1 = "6.0.0" webbrowser = "0.8" http-body-util = "0.1.2" regex = "1.11.1" diff --git a/crates/goose-mcp/src/google_drive/google_labels.rs b/crates/goose-mcp/src/google_drive/google_labels.rs deleted file mode 100644 index a2272ce48c9b..000000000000 --- a/crates/goose-mcp/src/google_drive/google_labels.rs +++ /dev/null @@ -1,478 +0,0 @@ -#![allow(clippy::ptr_arg, dead_code, clippy::enum_variant_names)] - -use std::collections::{BTreeSet, HashMap}; - -use google_apis_common as common; -use tokio::time::sleep; - -/// A scope is needed when requesting an -/// [authorization token](https://developers.google.com/workspace/drive/labels/guides/authorize). -#[derive(PartialEq, Eq, Ord, PartialOrd, Hash, Debug, Clone, Copy)] -pub enum Scope { - /// View, use, and manage Drive labels. - DriveLabels, - - /// View and use Drive labels. - DriveLabelsReadonly, - - /// View, edit, create, and delete all Drive labels in your organization, - /// and view your organization's label-related administration policies. - DriveLabelsAdmin, - - /// View all Drive labels and label-related administration policies in your - /// organization. - DriveLabelsAdminReadonly, -} - -impl AsRef for Scope { - fn as_ref(&self) -> &str { - match *self { - Scope::DriveLabels => "https://www.googleapis.com/auth/drive.labels", - Scope::DriveLabelsReadonly => "https://www.googleapis.com/auth/drive.labels.readonly", - Scope::DriveLabelsAdmin => "https://www.googleapis.com/auth/drive.admin.labels", - Scope::DriveLabelsAdminReadonly => { - "https://www.googleapis.com/auth/drive.admin.labels.readonly" - } - } - } -} - -#[allow(clippy::derivable_impls)] -impl Default for Scope { - fn default() -> Scope { - Scope::DriveLabelsReadonly - } -} - -#[derive(Clone)] -pub struct DriveLabelsHub { - pub client: common::Client, - pub auth: Box, - _user_agent: String, - _base_url: String, -} - -impl common::Hub for DriveLabelsHub {} - -impl<'a, C> DriveLabelsHub { - pub fn new( - client: common::Client, - auth: A, - ) -> DriveLabelsHub { - DriveLabelsHub { - client, - auth: Box::new(auth), - _user_agent: "google-api-rust-client/6.0.0".to_string(), - _base_url: "https://drivelabels.googleapis.com/".to_string(), - } - } - - pub fn labels(&'a self) -> LabelMethods<'a, C> { - LabelMethods { hub: self } - } - - /// Set the user-agent header field to use in all requests to the server. - /// It defaults to `google-api-rust-client/6.0.0`. - /// - /// Returns the previously set user-agent. - pub fn user_agent(&mut self, agent_name: String) -> String { - std::mem::replace(&mut self._user_agent, agent_name) - } - - /// Set the base url to use in all requests to the server. - /// It defaults to `https://www.googleapis.com/drive/v3/`. - /// - /// Returns the previously set base url. - pub fn base_url(&mut self, new_base_url: String) -> String { - std::mem::replace(&mut self._base_url, new_base_url) - } -} - -#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] -#[serde_with::serde_as] -#[derive(Default, Clone, Debug, serde::Serialize, serde::Deserialize)] -pub struct Label { - #[serde(rename = "name")] - pub name: Option, - #[serde(rename = "id")] - pub id: Option, - #[serde(rename = "revisionId")] - pub revision_id: Option, - #[serde(rename = "labelType")] - pub label_type: Option, - #[serde(rename = "creator")] - pub creator: Option, - #[serde(rename = "createTime")] - pub create_time: Option, - #[serde(rename = "revisionCreator")] - pub revision_creator: Option, - #[serde(rename = "revisionCreateTime")] - pub revision_create_time: Option, - #[serde(rename = "publisher")] - pub publisher: Option, - #[serde(rename = "publishTime")] - pub publish_time: Option, - #[serde(rename = "disabler")] - pub disabler: Option, - #[serde(rename = "disableTime")] - pub disable_time: Option, - #[serde(rename = "customer")] - pub customer: Option, - pub properties: Option, - pub fields: Option>, - // We ignore the remaining fields. -} - -impl common::Part for Label {} - -impl common::ResponseResult for Label {} - -#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] -#[serde_with::serde_as] -#[derive(Default, Clone, Debug, serde::Serialize, serde::Deserialize)] -pub struct LabelProperty { - pub title: Option, - pub description: Option, -} - -impl common::Part for LabelProperty {} - -#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] -#[serde_with::serde_as] -#[derive(Default, Clone, Debug, serde::Serialize, serde::Deserialize)] -pub struct Field { - id: Option, - #[serde(rename = "queryKey")] - query_key: Option, - properties: Option, - #[serde(rename = "selectionOptions")] - selection_options: Option, -} - -impl common::Part for Field {} - -#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] -#[serde_with::serde_as] -#[derive(Default, Clone, Debug, serde::Serialize, serde::Deserialize)] -pub struct FieldProperty { - #[serde(rename = "displayName")] - pub display_name: Option, - pub required: Option, -} - -impl common::Part for FieldProperty {} - -#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] -#[serde_with::serde_as] -#[derive(Default, Clone, Debug, serde::Serialize, serde::Deserialize)] -pub struct SelectionOption { - #[serde(rename = "listOptions")] - pub list_options: Option, - pub choices: Option>, -} - -impl common::Part for SelectionOption {} - -#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] -#[serde_with::serde_as] -#[derive(Default, Clone, Debug, serde::Serialize, serde::Deserialize)] -pub struct Choice { - id: Option, - properties: Option, - // We ignore the remaining fields. -} - -impl common::Part for Choice {} - -#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] -#[serde_with::serde_as] -#[derive(Default, Clone, Debug, serde::Serialize, serde::Deserialize)] -pub struct ChoiceProperties { - #[serde(rename = "displayName")] - display_name: Option, - description: Option, -} - -impl common::Part for ChoiceProperties {} - -#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] -#[serde_with::serde_as] -#[derive(Default, Clone, Debug, serde::Serialize, serde::Deserialize)] -pub struct LabelList { - pub labels: Option>, - #[serde(rename = "nextPageToken")] - pub next_page_token: Option, -} - -impl common::ResponseResult for LabelList {} - -/// Information about a Drive user. -/// -/// This type is not used in any activity, and only used as *part* of another schema. -/// -#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] -#[serde_with::serde_as] -#[derive(Default, Clone, Debug, serde::Serialize, serde::Deserialize)] -pub struct User { - /// Output only. A plain text displayable name for this user. - #[serde(rename = "displayName")] - pub display_name: Option, - /// Output only. The email address of the user. This may not be present in certain contexts if the user has not made their email address visible to the requester. - #[serde(rename = "emailAddress")] - pub email_address: Option, - /// Output only. Identifies what kind of resource this is. Value: the fixed string `"drive#user"`. - pub kind: Option, - /// Output only. Whether this user is the requesting user. - pub me: Option, - /// Output only. The user's ID as visible in Permission resources. - #[serde(rename = "permissionId")] - pub permission_id: Option, - /// Output only. A link to the user's profile photo, if available. - #[serde(rename = "photoLink")] - pub photo_link: Option, -} - -impl common::Part for User {} - -pub struct LabelMethods<'a, C> -where - C: 'a, -{ - hub: &'a DriveLabelsHub, -} - -impl common::MethodsBuilder for LabelMethods<'_, C> {} - -impl<'a, C> LabelMethods<'a, C> { - /// Create a builder to help you perform the following tasks: - /// - /// List labels - pub fn list(&self) -> LabelListCall<'a, C> { - LabelListCall { - hub: self.hub, - _delegate: Default::default(), - _additional_params: Default::default(), - _scopes: Default::default(), - } - } -} - -/// Lists the workspace's labels. -pub struct LabelListCall<'a, C> -where - C: 'a, -{ - hub: &'a DriveLabelsHub, - _delegate: Option<&'a mut dyn common::Delegate>, - _additional_params: HashMap, - _scopes: BTreeSet, -} - -impl common::CallBuilder for LabelListCall<'_, C> {} - -impl<'a, C> LabelListCall<'a, C> -where - C: common::Connector, -{ - /// Perform the operation you have built so far. - pub async fn doit(mut self) -> common::Result<(common::Response, LabelList)> { - use common::url::Params; - use hyper::header::{AUTHORIZATION, CONTENT_LENGTH, USER_AGENT}; - - let mut dd = common::DefaultDelegate; - let dlg: &mut dyn common::Delegate = self._delegate.unwrap_or(&mut dd); - dlg.begin(common::MethodInfo { - id: "drivelabels.labels.list", - http_method: hyper::Method::GET, - }); - - for &field in ["alt"].iter() { - if self._additional_params.contains_key(field) { - dlg.finished(false); - return Err(common::Error::FieldClash(field)); - } - } - - // TODO: We don't handle any of the query params. - let mut params = Params::with_capacity(2 + self._additional_params.len()); - - params.extend(self._additional_params.iter()); - - params.push("alt", "json"); - let url = self.hub._base_url.clone() + "v2/labels"; - - if self._scopes.is_empty() { - self._scopes - .insert(Scope::DriveLabelsReadonly.as_ref().to_string()); - } - - let url = params.parse_with_url(&url); - - loop { - let token = match self - .hub - .auth - .get_token(&self._scopes.iter().map(String::as_str).collect::>()[..]) - .await - { - Ok(token) => token, - Err(e) => match dlg.token(e) { - Ok(token) => token, - Err(e) => { - dlg.finished(false); - return Err(common::Error::MissingToken(e)); - } - }, - }; - let req_result = { - let client = &self.hub.client; - dlg.pre_request(); - let mut req_builder = hyper::Request::builder() - .method(hyper::Method::GET) - .uri(url.as_str()) - .header(USER_AGENT, self.hub._user_agent.clone()); - - if let Some(token) = token.as_ref() { - req_builder = req_builder.header(AUTHORIZATION, format!("Bearer {}", token)); - } - - let request = req_builder - .header(CONTENT_LENGTH, 0_u64) - .body(common::to_body::(None)); - client.request(request.unwrap()).await - }; - - match req_result { - Err(err) => { - if let common::Retry::After(d) = dlg.http_error(&err) { - sleep(d).await; - continue; - } - dlg.finished(false); - return Err(common::Error::HttpError(err)); - } - Ok(res) => { - let (parts, body) = res.into_parts(); - let body = common::Body::new(body); - if !parts.status.is_success() { - let bytes = common::to_bytes(body).await.unwrap_or_default(); - let error = serde_json::from_str(&common::to_string(&bytes)); - let response = common::to_response(parts, bytes.into()); - - if let common::Retry::After(d) = - dlg.http_failure(&response, error.as_ref().ok()) - { - sleep(d).await; - continue; - } - - dlg.finished(false); - - return Err(match error { - Ok(value) => common::Error::BadRequest(value), - _ => common::Error::Failure(response), - }); - } - let response = { - let bytes = common::to_bytes(body).await.unwrap_or_default(); - let encoded = common::to_string(&bytes); - match serde_json::from_str(&encoded) { - Ok(decoded) => (common::to_response(parts, bytes.into()), decoded), - Err(error) => { - dlg.response_json_decode_error(&encoded, &error); - return Err(common::Error::JsonDecodeError( - encoded.to_string(), - error, - )); - } - } - }; - - dlg.finished(true); - return Ok(response); - } - } - } - } - - /// The delegate implementation is consulted whenever there is an intermediate result, or if something goes wrong - /// while executing the actual API request. - /// - /// ````text - /// It should be used to handle progress information, and to implement a certain level of resilience. - /// ```` - /// - /// Sets the *delegate* property to the given value. - pub fn delegate(mut self, new_value: &'a mut dyn common::Delegate) -> LabelListCall<'a, C> { - self._delegate = Some(new_value); - self - } - - /// Set any additional parameter of the query string used in the request. - /// It should be used to set parameters which are not yet available through their own - /// setters. - /// - /// Please note that this method must not be used to set any of the known parameters - /// which have their own setter method. If done anyway, the request will fail. - /// - /// # Additional Parameters - /// - /// * *$.xgafv* (query-string) - V1 error format. - /// * *access_token* (query-string) - OAuth access token. - /// * *alt* (query-string) - Data format for response. - /// * *callback* (query-string) - JSONP - /// * *fields* (query-string) - Selector specifying which fields to include in a partial response. - /// * *key* (query-string) - API key. Your API key identifies your project and provides you with API access, quota, and reports. Required unless you provide an OAuth 2.0 token. - /// * *oauth_token* (query-string) - OAuth 2.0 token for the current user. - /// * *prettyPrint* (query-boolean) - Returns response with indentations and line breaks. - /// * *quotaUser* (query-string) - Available to use for quota purposes for server-side applications. Can be any arbitrary string assigned to a user, but should not exceed 40 characters. - /// * *uploadType* (query-string) - Legacy upload protocol for media (e.g. "media", "multipart"). - /// * *upload_protocol* (query-string) - Upload protocol for media (e.g. "raw", "multipart"). - pub fn param(mut self, name: T, value: T) -> LabelListCall<'a, C> - where - T: AsRef, - { - self._additional_params - .insert(name.as_ref().to_string(), value.as_ref().to_string()); - self - } - - /// Identifies the authorization scope for the method you are building. - /// - /// Use this method to actively specify which scope should be used, instead of the default [`Scope`] variant - /// [`Scope::DriveLabelsReadonly`]. - /// - /// The `scope` will be added to a set of scopes. This is important as one can maintain access - /// tokens for more than one scope. - /// - /// Usually there is more than one suitable scope to authorize an operation, some of which may - /// encompass more rights than others. For example, for listing resources, a *read-only* scope will be - /// sufficient, a read-write scope will do as well. - pub fn add_scope(mut self, scope: St) -> LabelListCall<'a, C> - where - St: AsRef, - { - self._scopes.insert(String::from(scope.as_ref())); - self - } - /// Identifies the authorization scope(s) for the method you are building. - /// - /// See [`Self::add_scope()`] for details. - pub fn add_scopes(mut self, scopes: I) -> LabelListCall<'a, C> - where - I: IntoIterator, - St: AsRef, - { - self._scopes - .extend(scopes.into_iter().map(|s| String::from(s.as_ref()))); - self - } - - /// Removes all scopes, and no default scope will be used either. - /// In this case, you have to specify your API-key using the `key` parameter (see [`Self::param()`] - /// for details). - pub fn clear_scopes(mut self) -> LabelListCall<'a, C> { - self._scopes.clear(); - self - } -} diff --git a/crates/goose-mcp/src/google_drive/mod.rs b/crates/goose-mcp/src/google_drive/mod.rs deleted file mode 100644 index adaa297a7e77..000000000000 --- a/crates/goose-mcp/src/google_drive/mod.rs +++ /dev/null @@ -1,3700 +0,0 @@ -mod google_labels; -mod oauth_pkce; -pub mod storage; - -use anyhow::{Context, Error}; -use base64::Engine; -use chrono::NaiveDate; -use indoc::indoc; -use lazy_static::lazy_static; -use mcp_core::{ - handler::{PromptError, ResourceError}, - protocol::ServerCapabilities, -}; -use mcp_server::router::CapabilitiesBuilder; -use mcp_server::Router; -use oauth_pkce::PkceOAuth2Client; -use regex::Regex; -use rmcp::model::{ - AnnotateAble, Content, ErrorCode, ErrorData, JsonRpcMessage, Prompt, RawResource, Resource, - Tool, ToolAnnotations, -}; -use rmcp::object; -use serde_json::{json, Value}; -use std::borrow::Cow; -use std::io::Cursor; -use std::{env, fs, future::Future, path::Path, pin::Pin, sync::Arc}; -use storage::CredentialsManager; -use tokio::sync::mpsc; - -use google_docs1::{self, Docs}; -use google_drive3::common::ReadSeek; -use google_drive3::{ - self, - api::{ - Comment, File, FileShortcutDetails, LabelFieldModification, LabelModification, - ModifyLabelsRequest, Permission, Reply, Scope, - }, - hyper_rustls::{self, HttpsConnector}, - hyper_util::{self, client::legacy::connect::HttpConnector}, - DriveHub, -}; -use google_labels::DriveLabelsHub; -use google_sheets4::{self, Sheets}; -use http_body_util::BodyExt; - -// Constants for credential storage -pub const KEYCHAIN_SERVICE: &str = "mcp_google_drive"; -pub const KEYCHAIN_USERNAME: &str = "oauth_credentials"; -pub const KEYCHAIN_DISK_FALLBACK_ENV: &str = "GOOGLE_DRIVE_DISK_FALLBACK"; - -const GOOGLE_DRIVE_SCOPES: Scope = Scope::Full; - -#[derive(Debug)] -enum FileOperation { - Create { name: String }, - Update { file_id: String }, -} -#[derive(PartialEq)] -enum PaginationState { - Start, - Next(String), - End, -} -const PERMISSIONTYPE: &[&str] = &["user", "group", "domain", "anyone"]; -const ROLES: &[&str] = &[ - "owner", - "organizer", - "fileOrganizer", - "writer", - "commenter", - "reader", -]; - -lazy_static! { - static ref GOOGLE_DRIVE_ID_REGEX: Regex = - Regex::new(r"^(?:https:\/\/)(?:[\w-]+\.)?google\.com\/(?:[^\/]+\/)*d\/([a-zA-Z0-9_-]+)") - .unwrap(); -} - -fn extract_google_drive_id(url: &str) -> Option<&str> { - GOOGLE_DRIVE_ID_REGEX - .captures(url) - .and_then(|caps| caps.get(1).map(|m| m.as_str())) -} - -pub struct GoogleDriveRouter { - tools: Vec, - instructions: String, - drive: DriveHub>, - drive_labels: DriveLabelsHub>, - sheets: Sheets>, - docs: Docs>, - credentials_manager: Arc, -} - -impl GoogleDriveRouter { - async fn google_auth() -> ( - DriveHub>, - DriveLabelsHub>, - Sheets>, - Docs>, - Arc, - ) { - let keyfile_path_str = env::var("GOOGLE_DRIVE_OAUTH_PATH") - .unwrap_or_else(|_| "./gcp-oauth.keys.json".to_string()); - let credentials_path_str = env::var("GOOGLE_DRIVE_CREDENTIALS_PATH") - .unwrap_or_else(|_| "./gdrive-server-credentials.json".to_string()); - - let expanded_keyfile = shellexpand::tilde(keyfile_path_str.as_str()); - let keyfile_path = Path::new(expanded_keyfile.as_ref()); - - let expanded_credentials = shellexpand::tilde(credentials_path_str.as_str()); - let credentials_path = expanded_credentials.to_string(); - - tracing::info!( - credentials_path = credentials_path_str, - keyfile_path = keyfile_path_str, - "Google Drive MCP server authentication config paths" - ); - - if let Ok(oauth_config) = env::var("GOOGLE_DRIVE_OAUTH_CONFIG") { - // Ensure the parent directory exists (create_dir_all is idempotent) - if let Some(parent) = keyfile_path.parent() { - if let Err(e) = fs::create_dir_all(parent) { - tracing::error!( - "Failed to create parent directories for {}: {}", - keyfile_path.display(), - e - ); - } - } - - // Check if the file exists and whether its content matches - // in every other case we attempt to overwrite - let need_to_write = match fs::read_to_string(keyfile_path) { - Ok(existing) if existing == oauth_config => false, - Ok(_) | Err(_) => true, - }; - - // Overwrite the file if needed - if need_to_write { - if let Err(e) = fs::write(keyfile_path, &oauth_config) { - tracing::error!( - "Failed to write OAuth config to {}: {}", - keyfile_path.display(), - e - ); - } else { - tracing::debug!( - "Wrote Google Drive MCP server OAuth config to {}", - keyfile_path.display() - ); - } - } - } - - // Check if we should fall back to disk, must be explicitly enabled - let fallback_to_disk = match env::var(KEYCHAIN_DISK_FALLBACK_ENV) { - Ok(value) => value.to_lowercase() == "true", - Err(_) => false, - }; - - // Create a credentials manager for storing tokens securely - let credentials_manager = Arc::new(CredentialsManager::new( - credentials_path.clone(), - fallback_to_disk, - KEYCHAIN_SERVICE.to_string(), - KEYCHAIN_USERNAME.to_string(), - )); - - // Read the OAuth credentials from the keyfile - match fs::read_to_string(keyfile_path) { - Ok(_) => { - // Create the PKCE OAuth2 client - let auth = PkceOAuth2Client::new(keyfile_path, credentials_manager.clone()) - .expect("Failed to create OAuth2 client"); - - // Create the HTTP client - let client = hyper_util::client::legacy::Client::builder( - hyper_util::rt::TokioExecutor::new(), - ) - .build( - hyper_rustls::HttpsConnectorBuilder::new() - .with_native_roots() - .unwrap() - .https_or_http() - .enable_http1() - .build(), - ); - - let drive_hub = DriveHub::new(client.clone(), auth.clone()); - let drive_labels_hub = DriveLabelsHub::new(client.clone(), auth.clone()); - let sheets_hub = Sheets::new(client.clone(), auth.clone()); - let docs_hub = Docs::new(client, auth); - - // Create and return the DriveHub, Sheets and our PKCE OAuth2 client - ( - drive_hub, - drive_labels_hub, - sheets_hub, - docs_hub, - credentials_manager, - ) - } - Err(e) => { - tracing::error!( - "Failed to read OAuth config from {}: {}", - keyfile_path.display(), - e - ); - panic!("Failed to read OAuth config: {}", e); - } - } - } - - pub async fn new() -> Self { - // handle auth - let (drive, drive_labels, sheets, docs, credentials_manager) = Self::google_auth().await; - - let search_tool = Tool::new( - "search".to_string(), - indoc! {r#" - List or search for files or labels in google drive by name, given an input search query. At least one of ('name', 'mimeType', or 'parent') are required for file searches. - "#} - .to_string(), - object!({ - "type": "object", - "properties": { - "driveType": { - "type": "string", - "description": "Required type of object to list or search (file, label)." - }, - "name": { - "type": "string", - "description": "String to search for in the file's name.", - }, - "mimeType": { - "type": "string", - "description": "Use when searching for a file to constrain the results to just this MIME type.", - }, - "parent": { - "type": "string", - "description": "ID of a folder to limit the search to", - }, - "driveId": { - "type": "string", - "description": "ID of a shared drive to constrain the search to when using the corpus 'drive'.", - }, - "corpora": { - "type": "string", - "description": "Which corpus to search, either 'user' (default), 'drive' (requires a driveID) or 'allDrives'", - }, - "pageSize": { - "type": "number", - "description": "How many items to return from the search query, default 10, max 100", - }, - "includeLabels": { - "type": "boolean", - "description": "When searching or listing files, also get any applied labels.", - } - }, - "required": ["driveType"], - }) - ).annotate(ToolAnnotations { - title: Some("Search GDrive".to_string()), - read_only_hint: Some(true), - destructive_hint: Some(false), - idempotent_hint: Some(false), - open_world_hint: Some(false), - }); - - let read_tool = Tool::new( - "read".to_string(), - indoc! {r#" - Read a file from google drive using the file URI or the full google drive URL. - One of URI or URL MUST is required. - - Optionally include base64 encoded images, false by default. - - Example extracting URIs from URLs: - Given "https://docs.google.com/document/d/1QG8d8wtWe7ZfmG93sW-1h2WXDJDUkOi-9hDnvJLmWrc/edit?tab=t.0#heading=h.5v419d3h97tr" - Pass in "gdrive:///1QG8d8wtWe7ZfmG93sW-1h2WXDJDUkOi-9hDnvJLmWrc" - Do not include any other path parameters when using URI. - "#} - .to_string(), - object!({ - "type": "object", - "properties": { - "uri": { - "type": "string", - "description": "google drive uri of the file to read, use this when you have the file URI", - }, - "url": { - "type": "string", - "description": "the full google drive URL to read the file from, use this when the user gives a full https url", - }, - "includeImages": { - "type": "boolean", - "description": "Whether or not to include images as base64 encoded strings, defaults to false", - } - }, - }) - ).annotate(ToolAnnotations { - title: Some("Read GDrive".to_string()), - read_only_hint: Some(true), - destructive_hint: Some(false), - idempotent_hint: Some(false), - open_world_hint: Some(false) - }); - - let create_file_tool = Tool::new( - "create_file".to_string(), - indoc! {r#" - Create a new file, including Document, Spreadsheet, Slides, folder, or shortcut, in Google Drive. - "#} - .to_string(), - object!({ - "type": "object", - "properties": { - "name": { - "type": "string", - "description": "Name of the file to create", - }, - "mimeType": { - "type": "string", - "description": "The MIME type of the file.", - }, - "body": { - "type": "string", - "description": "Text content for the file (required for document and spreadsheet types)", - }, - "path": { - "type": "string", - "description": "Path to a file to upload (required for slides type)", - }, - "parentId": { - "type": "string", - "description": "ID of the parent folder in which to create the file (default: creates files in the root of 'My Drive')", - }, - "targetId": { - "type": "string", - "description": "ID of the file to target when creating a shortcut", - }, - "allowSharedDrives": { - "type": "boolean", - "description": "Whether to allow access to shared drives or just your personal drive (default: false)", - } - }, - "required": ["name", "mimeType"], - }) - ).annotate(ToolAnnotations { - title: Some("Create new file in GDrive".to_string()), - read_only_hint: Some(false), - destructive_hint: Some(false), - idempotent_hint: Some(false), - open_world_hint: Some(false), - }); - - let move_file_tool = Tool::new( - "move_file".to_string(), - indoc! {r#" - Move a Google Drive file, folder, or shortcut to a new parent folder. You cannot move a folder to a different drive. - "#} - .to_string(), - object!({ - "type": "object", - "properties": { - "fileId": { - "type": "string", - "description": "The ID of the file to update.", - }, - "currentFolderId": { - "type": "string", - "description": "The ID of the current parent folder.", - }, - "newFolderId": { - "type": "string", - "description": "The ID of the folder to move the file to.", - }, - }, - "required": ["fileId", "currentFolderId", "newFolderId"], - }) - ).annotate(ToolAnnotations { - title: Some("Move file".to_string()), - read_only_hint: Some(false), - destructive_hint: Some(true), - idempotent_hint: Some(false), - open_world_hint: Some(false), - }); - - let update_file_tool = Tool::new( - "update_file".to_string(), - indoc! {r#" - Update an existing file in Google Drive with new content or edit the file's labels. - "#} - .to_string(), - object!({ - "type": "object", - "properties": { - "fileId": { - "type": "string", - "description": "The ID of the file to update.", - }, - "allowSharedDrives": { - "type": "boolean", - "description": "Whether to allow access to shared drives or just your personal drive (default: false)", - }, - "mimeType": { - "type": "string", - "description": "The MIME type of the file.", - }, - "body": { - "type": "string", - "description": "Plain text body of the file to upload. Mutually exclusive with path (required for Google Document and Google Spreadsheet types).", - }, - "path": { - "type": "string", - "description": "Path to a local file to use to update the Google Drive file. Mutually exclusive with body (required for Google Slides type)", - }, - "updateLabels": { - "type": "array", - "description": "Array of label operations to perform on the file. Each operation may remove one label, unset one field, or update one field.", - "items": { - "type": "object", - "properties": { - "labelId": { - "type": "string", - "description": "The ID of the label to be operated upon." - }, - "operation": { - "type": "string", - "enum": ["removeLabel", "unsetField", "addOrUpdateLabel"], - "description": "The operation to perform. You may 'removeLabel' to completely remove the label from the file, 'unsetField' to remove a field from an applied label, or 'addOrUpdateLabel' to add a new label (with or without fields), or change the value of a field on an applied label." - }, - "fieldId": { - "type": "string", - "description": "The ID of the field to be operated upon." - }, - "dateValue": { - "type": "array", - "description": "If updating a date field, an array of RFC 3339 dates (format YYYY-MM-DD) to update to.", - "items": { - "type": "string", - "description": "An RFC 3339 full-date format YYYY-MM-DD.", - } - }, - "textValue": { - "type": "array", - "description": "If updating a text field, the string values to update to.", - "items": { - "type": "string", - "description": "Text field values.", - } - }, - "choiceValue": { - "type": "array", - "description": "If updating a Choice field, the ID(s) of the desired choice field(s).", - "items": { - "type": "string", - "description": "Choice ID as a string", - } - }, - "integerValue": { - "type": "array", - "description": "If updating an integer field, the integer values to use.", - "items": { - "type": "integer", - "description": "The integer value.", - } - }, - "userValue": { - "type": "array", - "description": "If updating a user field, an array of the email address(es) of the user(s) to set as the field value.", - "items": { - "type": "string", - "description": "Email address as a string", - } - } - } - } - }, - }, - "required": ["fileId"], - "dependentRequired": { - "body": ["mimeType"], - "path": ["mimeType"] - } - }) - ).annotate(ToolAnnotations { - title: Some("Update a file's contents or labels".to_string()), - read_only_hint: Some(false), - destructive_hint: Some(true), - idempotent_hint: Some(false), - open_world_hint: Some(false), - }); - - let sheets_tool = Tool::new( - "sheets_tool".to_string(), - indoc! {r#" - Work with Google Sheets data using various operations. - Supports operations: - - list_sheets: List all sheets in a spreadsheet - - get_columns: Get column headers from a specific sheet - - get_values: Get values from a range - - update_values: Update values in a range - - update_cell: Update a single cell value - - add_sheet: Add a new sheet (tab) to a spreadsheet - - clear_values: Clear values from a range - "#} - .to_string(), - object!({ - "type": "object", - "properties": { - "spreadsheetId": { - "type": "string", - "description": "The ID of the spreadsheet to work with", - }, - "operation": { - "type": "string", - "enum": ["list_sheets", "get_columns", "get_values", "update_values", "update_cell", "add_sheet", "clear_values"], - "description": "The operation to perform on the spreadsheet", - }, - "sheetName": { - "type": "string", - "description": "The name of the sheet to work with (optional for some operations)", - }, - "range": { - "type": "string", - "description": "The A1 notation of the range to retrieve or update values (e.g., 'Sheet1!A1:D10')", - }, - "values": { - "type": "string", - "description": "CSV formatted data for update operations (required for update_values)", - }, - "cell": { - "type": "string", - "description": "The A1 notation of the cell to update (e.g., 'Sheet1!A1') for update_cell operation", - }, - "value": { - "type": "string", - "description": "The value to set in the cell for update_cell operation", - }, - "title": { - "type": "string", - "description": "Title for the new sheet (required for add_sheet)", - }, - "valueInputOption": { - "type": "string", - "enum": ["RAW", "USER_ENTERED"], - "description": "How input data should be interpreted (default: USER_ENTERED)", - } - }, - "required": ["spreadsheetId", "operation"], - }) - ).annotate(ToolAnnotations { - title: Some("Work with Google Sheets data using various operations.".to_string()), - read_only_hint: Some(false), - destructive_hint: Some(true), - idempotent_hint: Some(false), - open_world_hint: Some(false), - }); - - let docs_tool = Tool::new( - "docs_tool".to_string(), - indoc! {r#" - Work with Google Docs data using various operations. - Supports operations: - - get_document: Get the full document content - - insert_text: Insert text at a specific location - - append_text: Append text to the end of the document - - replace_text: Replace all instances of text - - create_paragraph: Create a new paragraph - - delete_content: Delete content between positions - "#} - .to_string(), - object!({ - "type": "object", - "properties": { - "documentId": { - "type": "string", - "description": "The ID of the document to work with", - }, - "operation": { - "type": "string", - "enum": ["get_document", "insert_text", "append_text", "replace_text", "create_paragraph", "delete_content"], - "description": "The operation to perform on the document", - }, - "text": { - "type": "string", - "description": "The text to insert, append, or use for replacement", - }, - "replaceText": { - "type": "string", - "description": "The text to be replaced", - }, - "position": { - "type": "number", - "description": "The position in the document (index) for operations that require a position", - }, - "startPosition": { - "type": "number", - "description": "The start position for delete_content operation", - }, - "endPosition": { - "type": "number", - "description": "The end position for delete_content operation", - } - }, - "required": ["documentId", "operation"], - }) - ).annotate(ToolAnnotations { - title: Some("Work with Google Docs data using various operations.".to_string()), - read_only_hint: Some(false), - destructive_hint: Some(true), - idempotent_hint: Some(false), - open_world_hint: Some(false), - }); - - let get_comments_tool = Tool::new( - "get_comments".to_string(), - indoc! {r#" - List comments for a file in google drive. - "#} - .to_string(), - object!({ - "type": "object", - "properties": { - "fileId": { - "type": "string", - "description": "Id of the file to list comments for.", - } - }, - "required": ["fileId"], - }), - ) - .annotate(ToolAnnotations { - title: Some("List file comments".to_string()), - read_only_hint: Some(true), - destructive_hint: Some(false), - idempotent_hint: Some(false), - open_world_hint: Some(false), - }); - - let manage_comment_tool = Tool::new( - "manage_comment".to_string(), - indoc! {r#" - Manage comment for a Google Drive file. - - Supports the operations: - - create: Create a comment for the latest revision of a Google Drive file. The Google Drive API only supports unanchored comments (they don't refer to a specific location in the file). - - reply: Add a reply to a comment thread, or resolve a comment. - "#} - .to_string(), - object!({ - "type": "object", - "properties": { - "fileId": { - "type": "string", - "description": "Id of the file.", - }, - "operation": { - "type": "string", - "description": "Desired comment management operation.", - "enum": ["create", "reply"], - }, - "content": { - "type": "string", - "description": "Content of the comment to create or reply.", - }, - "commentId": { - "type": "string", - "description": "Id of the comment to which you'd like to reply. ", - }, - "resolveComment": { - "type": "boolean", - "description": "Whether to resolve the comment in reply. Defaults to false.", - } - }, - "required": ["fileId", "operation", "content"], - }) - ).annotate(ToolAnnotations { - title: Some("Manage file comment".to_string()), - read_only_hint: Some(false), - destructive_hint: Some(false), - idempotent_hint: Some(false), - open_world_hint: Some(false), - }); - - let list_drives_tool = Tool::new( - "list_drives".to_string(), - indoc! {r#" - List shared Google drives. - "#} - .to_string(), - object!({ - "type": "object", - "properties": { - "name_contains": { - "type": "string", - "description": "Optional name to search for when listing drives.", - } - }, - }), - ) - .annotate(ToolAnnotations { - title: Some("List shared google drives".to_string()), - read_only_hint: Some(true), - destructive_hint: Some(false), - idempotent_hint: Some(false), - open_world_hint: Some(false), - }); - - let get_permissions_tool = Tool::new( - "get_permissions".to_string(), - indoc! {r#" - List sharing permissions for a file, folder, or shared drive. - "#} - .to_string(), - object!({ - "type": "object", - "properties": { - "fileId": { - "type": "string", - "description": "Id of the file, folder, or shared drive.", - } - }, - "required": ["fileId"], - }), - ) - .annotate(ToolAnnotations { - title: Some("List sharing permissions".to_string()), - read_only_hint: Some(true), - destructive_hint: Some(false), - idempotent_hint: Some(false), - open_world_hint: Some(false), - }); - - let sharing_tool = Tool::new( - "sharing".to_string(), - indoc! {r#" - Manage sharing for a Google Drive file or folder. - - Supports the operations: - - create: Create a new permission for a 'type' identified by the 'target' param to have the 'role' privileges. - - update: Update an existing permission to a different role. (You cannot change the type or to whom it is targeted). - - delete: Delete an existing permission. - "#} - .to_string(), - object!({ - "type": "object", - "properties": { - "fileId": { - "type": "string", - "description": "Id of the file or folder.", - }, - "operation": { - "type": "string", - "description": "Desired sharing operation.", - "enum": ["create", "update", "delete"], - }, - "permissionId": { - "type": "string", - "description": "Permission Id for delete or update operations.", - }, - "role": { - "type": "string", - "description": "Role to apply to permission for create or update operations.", - "enum": ["owner", "organizer", "fileOrganizer", "writer", "commenter", "reader"] - }, - "type": { - "type": "string", - "description": "Type of permission to create or update.", - "enum": ["user", "group", "domain", "anyone"], - }, - "target": { - "type": "string", - "description": "For the user and group types, the email address. For a domain type, the domain name. (The anyone type does not require a target). Required for the create operation.", - }, - "emailMessage": { - "type": "string", - "description": "Email notification message to send to users and groups.", - }, - }, - "required": ["fileId", "operation"], - }) - ).annotate(ToolAnnotations { - title: Some("Manage file sharing".to_string()), - read_only_hint: Some(false), - destructive_hint: Some(false), - idempotent_hint: Some(false), - open_world_hint: Some(false), - }); - - let instructions = indoc::formatdoc! {r#" - Google Drive MCP Server Instructions - - ## Overview - The Google Drive MCP server provides tools for interacting with Google Drive files, Google Sheets, and Google Docs: - 1. search - List or search for files or labels in your Google Drive - 2. read - Read file contents directly using a uri in the `gdrive:///uri` format - 3. move_file - Move a file to a new location in Google Drive - 4. list_drives - List the shared drives to which you have access - 5. get_permissions - List the permissions of a file or folder - 6. sharing - Share a file or folder with others - 7. get_comments - List a file or folder's comments - 8. manage_comment - Manage comment for a Google Drive file. - 9. create_file - Create a new file - 10. update_file - Update an existing file's contents or labels - 11. sheets_tool - Work with Google Sheets data using various operations - 12. docs_tool - Work with Google Docs data using various operations - - ## Available Tools - - ### 1. Search Tool - Search for or list files or labels in Google Drive. Files are - searched by name and ordered by most recently viewedByMeTime. - A corpora parameter controls which corpus is searched. - Returns: List of files with their names, MIME types, and IDs or a - list of labels and their fields. - - ### 2. Read File Tool - Read a file's contents using its ID, and optionally include images as base64 encoded data. - The default is to exclude images, to include images set includeImages to true in the query. - - Example mappings for Google Drive resources to `gdrive:///$URI` format: - - Google Document File: - Example URL: https://docs.google.com/document/d/1QG8d8wtWe7ZfmG93sW-1h2WXDJDUkOi-9hDnvJLmWrc/edit?tab=t.0#heading=h.5v419d3h97tr - URI Format: gdrive:///1QG8d8wtWe7ZfmG93sW-1h2WXDJDUkOi-9hDnvJLmWrc - - - Google Sheet: - Example URL: https://docs.google.com/spreadsheets/d/1J5KHqWsGFzweuiQboX7dlm8Ejv90Po16ocEBahzCt4W/edit?gid=1249300797#gid=1249300797 - URI Format: gdrive:///1J5KHqWsGFzweuiQboX7dlm8Ejv90Po16ocEBahzCt4W - - - Google Slides: - Example URL: https://docs.google.com/presentation/d/1zXWqsGpHJEu40oqb1omh68sW9liu7EKFBCdnPaJVoQ5et/edit#slide=id.p1 - URI Format: gdrive:///1zXWqsGpHJEu40oqb1omh68sW9liu7EKFBCdnPaJVoQ5et - - Images take up a large amount of context, this should only be used if a - user explicity needs the image data. - - Limitations: Google Sheets exporting only supports reading the first sheet. This is an important limitation that should - be communicated to the user whenever dealing with a Google Sheet (mimeType: application/vnd.google-apps.spreadsheet). - - #### File Format Handling - The read file tool's output will be converted: - - Google Docs → Markdown - - Google Sheets → CSV - - Google Presentations → Plain text - - Text/JSON files → UTF-8 text - - Binary files → Base64 encoded - - ### 3. Move File Tool - Move a file from its current folder to a new folder, including folders on another drive. - - ### 4. List Drives Tool - Lists the user's available Shared Drives. - - ### 5. Get Permissions Tool - Lists the permissions for a file or folder. Permissions in Google - Drive consist of a type ('user', 'group', 'domain', 'anyone') and a role - ('owner', 'organizer', 'fileOrganizer', 'writer', 'commenter', - 'reader'). - - ### 6. Sharing Tool - Create a new permission, update the role on an existing permission, - or delete a permission. User, group, and domain permissions should - have a provided "target" email address or domain name. - - ### 7. Get Comments Tool - Lists the comments for a Google Workspace file. - - ### 8. Manage Comment Tool - Create or reply comment for a Google Drive file. - - ### 9. Create File Tool - Create any kind of file, including Google Workspace files (Docs, Sheets, or Slides) directly in Google Drive. - - For Google Docs: Converts Markdown text to a Google Document - - For Google Sheets: Converts CSV text to a Google Spreadsheet - - For Google Slides: Converts a PowerPoint file to Google Slides (requires a path to the powerpoint file) - - Other: No file conversion. - - *Note*: All updates overwrite the existing content with the new - content provided. To modify specific parts of the document, you must - include the changes as part of the entire document. - - ### 10. Update File Tool - Replace the entire contents of an existing file with new content, - including Google Workspace files (Docs, Sheets, or Slides), or - update the labels applied to a file. - - For Google Docs: Updates with new Markdown text - - For Google Sheets: Updates with new CSV text - - For Google Slides: Updates with a new PowerPoint file (requires a path to the powerpoint file) - - Other: No file conversion. - - Label operations include adding a new label, unsetting a field for - an already-applied label, removing a label, or changing the field - value for an applied label. - - ### 11. Sheets Tool - Work with Google Sheets data using various operations: - - list_sheets: List all sheets in a spreadsheet - - get_columns: Get column headers from a specific sheet - - get_values: Get values from a range - - update_values: Update values in a range (requires CSV formatted data) - - update_cell: Update a single cell value - - add_sheet: Add a new sheet (tab) to a spreadsheet - - clear_values: Clear values from a range - - For update_values operation, provide CSV formatted data in the values parameter. - Each line represents a row, with values separated by commas. - Example: "John,Doe,30\nJane,Smith,25" - - For update_cell operation, provide the cell reference (e.g., 'Sheet1!A1') and the value to set. - - Parameters: - - spreadsheetId: The ID of the spreadsheet (can be obtained from search results) - - operation: The operation to perform (one of the operations listed above) - - sheetName: The name of the sheet to work with (optional for some operations) - - range: The A1 notation of the range to retrieve or update values (e.g., 'Sheet1!A1:D10') - - values: CSV formatted data for update operations - - cell: The A1 notation of the cell to update (e.g., 'Sheet1!A1') for update_cell operation - - value: The value to set in the cell for update_cell operation - - title: Title for the new sheet (required for add_sheet operation) - - valueInputOption: How input data should be interpreted (RAW or USER_ENTERED) - - ### 12. Docs Tool - Work with Google Docs data using various operations: - - get_document: Get the full document content - - insert_text: Insert text at a specific location - - append_text: Append text to the end of the document - - replace_text: Replace all instances of text - - create_paragraph: Create a new paragraph - - delete_content: Delete content between positions - - Parameters: - - documentId: The ID of the document (can be obtained from search results) - - operation: The operation to perform (one of the operations listed above) - - text: The text to insert, append, or use for replacement - - replaceText: The text to be replaced (for replace_text operation) - - position: The position in the document (index) for operations that require a position - - startPosition: The start position for delete_content operation - - endPosition: The end position for delete_content operation - - ## Common Usage Pattern - - 1. First, search for the file you want to read, searching by name. - 2. Then, use the file URI from the search results to read its contents. - 3. For Google Sheets, use the sheets_tool with the appropriate operation. - 4. For Google Docs, use the docs_tool with the appropriate operation. - - ## Best Practices - 1. Always use search first to find the correct file URI - 2. Search results include file types (MIME types) to help identify the right file - 3. Search is limited to 10 results per query, so use specific search terms - 4. When updating sheet values, format the data as CSV with one row per line - - ## Error Handling - If you encounter errors: - 1. Verify the file URI is correct - 2. Ensure you have access to the file - 3. Check if the file format is supported - 4. Verify the server is properly configured - - Remember: Always use the tools in sequence - search first to get the file URI, then read to access the contents. - "#}; - - Self { - tools: vec![ - search_tool, - read_tool, - create_file_tool, - move_file_tool, - update_file_tool, - sheets_tool, - docs_tool, - get_comments_tool, - manage_comment_tool, - list_drives_tool, - get_permissions_tool, - sharing_tool, - ], - instructions, - drive, - drive_labels, - sheets, - docs, - credentials_manager, - } - } - - // Implement search tool functionality - async fn search(&self, params: Value) -> Result, ErrorData> { - // To minimize tool growth, we search/list for a number of different - // objects in Gdrive with sub-funcs. - let drive_type = params - .get("driveType") - .and_then(|q| q.as_str()) - .ok_or_else(|| ErrorData { - code: ErrorCode::INVALID_PARAMS, - message: Cow::from("The type is required".to_string()), - data: None, - })?; - match drive_type { - "file" => return self.search_files(params).await, - "label" => return self.list_labels(params).await, - t => Err(ErrorData { - code: ErrorCode::INVALID_PARAMS, - message: Cow::from(format!( - "type must be one of (\'file\', \'label\'), got {}", - t - )), - data: None, - }), - } - } - - async fn search_files(&self, params: Value) -> Result, ErrorData> { - let name = params.get("name").and_then(|q| q.as_str()); - let mime_type = params.get("mimeType").and_then(|q| q.as_str()); - let drive_id = params.get("driveId").and_then(|q| q.as_str()); - let parent = params.get("parent").and_then(|q| q.as_str()); - - // extract corpora query parameter, validate options, or default to "user" - let corpus = params - .get("corpora") - .and_then(|c| c.as_str()) - .map(|s| { - if ["user", "drive", "allDrives"].contains(&s) { - Ok(s) - } else { - Err(ErrorData { - code: ErrorCode::INVALID_PARAMS, - message: Cow::from(format!( - "corpora must be either 'user', 'drive', or 'allDrives', got {}", - s - )), - data: None, - }) - } - }) - .unwrap_or(Ok("user"))?; - - // extract pageSize, and convert it to an i32, default to 10 - let page_size: i32 = params - .get("pageSize") - .map(|s| { - s.as_i64() - .and_then(|n| i32::try_from(n).ok()) - .ok_or_else(|| ErrorData { - code: ErrorCode::INVALID_PARAMS, - message: Cow::from(format!("Invalid pageSize: {}", s)), - data: None, - }) - .and_then(|n| { - if (0..=100).contains(&n) { - Ok(n) - } else { - Err(ErrorData { - code: ErrorCode::INVALID_PARAMS, - message: Cow::from(format!( - "pageSize must be between 0 and 100, got {}", - n - )), - data: None, - }) - } - }) - }) - .unwrap_or(Ok(10))?; - - let include_labels = params - .get("includeLabels") - .and_then(|b| b.as_bool()) - .unwrap_or(false); - - let mut query = Vec::new(); - if let Some(n) = name { - query.push( - format!( - "name contains '{}'", - n.replace('\\', "\\\\").replace('\'', "\\'") - ) - .to_string(), - ); - } - if let Some(m) = mime_type { - query.push(format!("mimeType = '{}'", m).to_string()); - } - if let Some(p) = parent { - query.push(format!("'{}' in parents", p).to_string()); - } - let query_string = query.join(" and "); - if query_string.is_empty() { - return Err(ErrorData { - code: ErrorCode::INVALID_PARAMS, - message: Cow::from("No query provided. Please include one of (\'name\', \'mimeType\', \'parent\').".to_string()), - data: None - }); - } - let mut builder = self - .drive - .files() - .list() - .corpora(corpus) - .q(query_string.as_str()) - .order_by("viewedByMeTime desc") - .param( - "fields", - &format!( - "files(id, name, mimeType, modifiedTime, size{})", - if include_labels { ", labelInfo" } else { "" } - ), - ) - .page_size(page_size) - .supports_all_drives(true) - .include_items_from_all_drives(true) - .clear_scopes() // Scope::MeetReadonly is the default, remove it - .add_scope(GOOGLE_DRIVE_SCOPES); - // You can only use the drive_id param when the corpus is "drive". - if let (Some(d), "drive") = (drive_id, corpus) { - builder = builder.drive_id(d); - } - // If we want labels, we have to go look up the IDs first. - // let mut label_results: Vec