everruns_integrations_parallel/
connection.rs1use 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}