Skip to main content

everruns_integrations_parallel/
connection.rs

1// Parallel Connection Provider
2//
3// Decision: API-key connection. Parallel's MCP accepts the key as a bearer
4// token on both /mcp and /mcp-oauth, and the free mode remains usable without
5// storing any credential.
6
7use async_trait::async_trait;
8use everruns_core::connector::{
9    Connector, ConnectorFormSchema, ConnectorType, ConnectorValidation, FormField,
10};
11use everruns_core::{McpToolsListRequest, McpToolsListResponse};
12
13use crate::{PARALLEL_MCP_URL, PARALLEL_PROVIDER_ID};
14
15pub struct ParallelConnector;
16
17#[async_trait]
18impl Connector for ParallelConnector {
19    fn provider_id(&self) -> &str {
20        PARALLEL_PROVIDER_ID
21    }
22
23    fn display_name(&self) -> &str {
24        "Parallel"
25    }
26
27    fn description(&self) -> &str {
28        "Optional Parallel API key for higher Search MCP limits and authenticated MCP endpoints"
29    }
30
31    fn icon(&self) -> &str {
32        "search"
33    }
34
35    fn connection_type(&self) -> ConnectorType {
36        ConnectorType::ApiKey
37    }
38
39    fn form_schema(&self) -> Option<ConnectorFormSchema> {
40        Some(ConnectorFormSchema {
41            fields: vec![
42                FormField::password("api_key", "API Key")
43                    .required()
44                    .with_placeholder("parallel_api_key")
45                    .with_help("Parallel is free without a key; add one for authenticated usage."),
46            ],
47            instructions_markdown: "\
48Parallel MCP works without a key by default.\n\n\
49To use authenticated mode:\n\
501. Go to [Parallel Platform](https://platform.parallel.ai)\n\
512. Create or copy your API key\n\
523. Paste it below"
53                .to_string(),
54        })
55    }
56
57    async fn validate(&self, credential: &str) -> Result<ConnectorValidation, String> {
58        let response = reqwest::Client::new()
59            .post(PARALLEL_MCP_URL)
60            .bearer_auth(credential)
61            .json(&McpToolsListRequest::default())
62            .send()
63            .await
64            .map_err(|e| format!("Failed to reach Parallel MCP: {e}"))?;
65
66        match response.status().as_u16() {
67            200 => {
68                let mcp_response: McpToolsListResponse = response
69                    .json()
70                    .await
71                    .map_err(|e| format!("Invalid Parallel MCP response: {e}"))?;
72                if let Some(error) = mcp_response.error {
73                    return Err(format!("Parallel MCP error: {}", error.message));
74                }
75                Ok(ConnectorValidation {
76                    provider_username: None,
77                    provider_metadata: None,
78                })
79            }
80            401 | 403 => Err("Invalid Parallel API key.".to_string()),
81            429 => Err("Parallel API key is valid but rate-limited. Try again later.".to_string()),
82            status => Err(format!(
83                "Unexpected response from Parallel MCP (HTTP {status})"
84            )),
85        }
86    }
87}
88
89#[cfg(test)]
90mod tests {
91    use super::*;
92
93    #[test]
94    fn provider_metadata() {
95        let p = ParallelConnector;
96        assert_eq!(p.provider_id(), "parallel");
97        assert_eq!(p.display_name(), "Parallel");
98        assert_eq!(p.connection_type(), ConnectorType::ApiKey);
99        assert_eq!(p.icon(), "search");
100    }
101
102    #[test]
103    fn form_schema_describes_optional_key() {
104        let p = ParallelConnector;
105        let schema = p.form_schema().expect("should have form schema");
106        assert_eq!(schema.fields.len(), 1);
107        assert_eq!(schema.fields[0].name, "api_key");
108        assert!(schema.instructions_markdown.contains("without a key"));
109        assert!(
110            schema
111                .instructions_markdown
112                .contains("platform.parallel.ai")
113        );
114    }
115}