Add tempguru event staffing tool plugin#3252
Conversation
Wraps the public TempGuru MCP REST API as 5 read-only Dify tools: - get_cities : list cities by state/tier - get_roles : list event staffing roles - check_availability : lead-time guidance for a city + date - get_role_pricing : all-inclusive hourly rate range - get_compliance_by_state : state-level wage/overtime/compliance Architecture: each tool is a 20-line Python class that makes one HTTPS GET to mcp.tempguru.co/api/v1/<endpoint> and yields the response JSON back to Dify. No business logic reimplemented; the MCP REST mirror is the canonical source. Trust profile: - Read-only (every tool is a GET) - No authentication required (public data) - No API keys, OAuth, credentials - No user data persisted - No executable scripts beyond the standard Dify entrypoint Matches the no-auth precedent set by duckduckgo and wikipedia plugins in the same .curated directory. Bilingual labels (en_US + zh_Hans) throughout.
There was a problem hiding this comment.
Code Review
This pull request introduces the TempGuru Event Staffing Dify plugin, which provides read-only access to event staffing data across US and Canadian markets through five tools. The feedback focuses on improving the robustness and safety of the plugin. Specifically, it is recommended to simplify credential validation by using a direct HTTP request rather than invoking internal SDK methods, and to clean up the resulting unused import. Additionally, the reviewer suggests implementing defensive parameter checks and wrapping API requests in try-except blocks across all tools to gracefully handle network errors and JSON parsing failures.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| def _validate_credentials(self, credentials: dict) -> None: | ||
| try: | ||
| for _ in GetCitiesTool.from_credentials(credentials).invoke_from_executor( | ||
| tool_parameters={} | ||
| ): | ||
| pass | ||
| except Exception as e: | ||
| raise ToolProviderCredentialValidationError( | ||
| f"Unable to reach TempGuru MCP REST API at mcp.tempguru.co: {str(e)}" | ||
| ) |
There was a problem hiding this comment.
Instantiating a tool class and calling internal SDK methods like invoke_from_executor inside _validate_credentials is highly risky and prone to breaking if the SDK internals change. It is much safer and cleaner to perform a direct HTTP request to the public API endpoint to verify connectivity.
| def _validate_credentials(self, credentials: dict) -> None: | |
| try: | |
| for _ in GetCitiesTool.from_credentials(credentials).invoke_from_executor( | |
| tool_parameters={} | |
| ): | |
| pass | |
| except Exception as e: | |
| raise ToolProviderCredentialValidationError( | |
| f"Unable to reach TempGuru MCP REST API at mcp.tempguru.co: {str(e)}" | |
| ) | |
| def _validate_credentials(self, credentials: dict) -> None: | |
| try: | |
| response = requests.get( | |
| "https://mcp.tempguru.co/api/v1/cities", | |
| headers={"User-Agent": "tempguru-dify-plugin/0.0.1", "Accept": "application/json"}, | |
| timeout=10, | |
| ) | |
| response.raise_for_status() | |
| except Exception as e: | |
| raise ToolProviderCredentialValidationError( | |
| f"Unable to reach TempGuru MCP REST API at mcp.tempguru.co: {str(e)}" | |
| ) |
| from dify_plugin.errors.tool import ToolProviderCredentialValidationError | ||
| from dify_plugin import ToolProvider | ||
| from tools.get_cities import GetCitiesTool |
There was a problem hiding this comment.
The import of GetCitiesTool is unused if we simplify the credential validation logic to use a direct HTTP request. Removing it keeps the imports clean and avoids potential import cycle or path resolution issues.
| from dify_plugin.errors.tool import ToolProviderCredentialValidationError | |
| from dify_plugin import ToolProvider | |
| from tools.get_cities import GetCitiesTool | |
| from dify_plugin.errors.tool import ToolProviderCredentialValidationError | |
| from dify_plugin import ToolProvider | |
| import requests |
| def _invoke( | ||
| self, tool_parameters: dict[str, Any] | ||
| ) -> Generator[ToolInvokeMessage, None, None]: | ||
| params: dict[str, Any] = { | ||
| "city": tool_parameters["city"], | ||
| "date": tool_parameters["date"], | ||
| } | ||
| if tool_parameters.get("role"): | ||
| params["role"] = tool_parameters["role"] | ||
| if tool_parameters.get("count"): | ||
| params["count"] = tool_parameters["count"] | ||
|
|
||
| response = requests.get( | ||
| f"{API_BASE}/availability", | ||
| params=params, | ||
| headers={"User-Agent": USER_AGENT, "Accept": "application/json"}, | ||
| timeout=30, | ||
| ) | ||
| response.raise_for_status() | ||
| yield self.create_json_message(response.json()) |
There was a problem hiding this comment.
To ensure robust error handling and defensive programming, we should safely extract parameters using .get() and check for their presence. Additionally, wrapping the HTTP request in a try-except block to catch requests.RequestException and ValueError prevents unhandled exceptions from crashing the tool execution, allowing us to return a clean error message instead.
def _invoke(
self, tool_parameters: dict[str, Any]
) -> Generator[ToolInvokeMessage, None, None]:
city = tool_parameters.get("city")
date = tool_parameters.get("date")
if not city or not date:
yield self.create_text_message("Both 'city' and 'date' parameters are required.")
return
params: dict[str, Any] = {
"city": city,
"date": date,
}
if tool_parameters.get("role"):
params["role"] = tool_parameters["role"]
if tool_parameters.get("count"):
params["count"] = tool_parameters["count"]
try:
response = requests.get(
f"{API_BASE}/availability",
params=params,
headers={"User-Agent": USER_AGENT, "Accept": "application/json"},
timeout=30,
)
response.raise_for_status()
yield self.create_json_message(response.json())
except requests.RequestException as e:
yield self.create_text_message(f"API request failed: {str(e)}")
except ValueError:
yield self.create_text_message("Failed to parse response from TempGuru API.")| def _invoke( | ||
| self, tool_parameters: dict[str, Any] | ||
| ) -> Generator[ToolInvokeMessage, None, None]: | ||
| params: dict[str, Any] = {} | ||
| if tool_parameters.get("state"): | ||
| params["state"] = tool_parameters["state"] | ||
| if tool_parameters.get("tier"): | ||
| params["tier"] = tool_parameters["tier"] | ||
|
|
||
| response = requests.get( | ||
| f"{API_BASE}/cities", | ||
| params=params, | ||
| headers={"User-Agent": USER_AGENT, "Accept": "application/json"}, | ||
| timeout=30, | ||
| ) | ||
| response.raise_for_status() | ||
| yield self.create_json_message(response.json()) |
There was a problem hiding this comment.
Wrapping the network request in a try-except block ensures that any connection issues, timeouts, or invalid JSON responses are handled gracefully rather than raising unhandled exceptions.
def _invoke(
self, tool_parameters: dict[str, Any]
) -> Generator[ToolInvokeMessage, None, None]:
params: dict[str, Any] = {}
if tool_parameters.get("state"):
params["state"] = tool_parameters["state"]
if tool_parameters.get("tier"):
params["tier"] = tool_parameters["tier"]
try:
response = requests.get(
f"{API_BASE}/cities",
params=params,
headers={"User-Agent": USER_AGENT, "Accept": "application/json"},
timeout=30,
)
response.raise_for_status()
yield self.create_json_message(response.json())
except requests.RequestException as e:
yield self.create_text_message(f"API request failed: {str(e)}")
except ValueError:
yield self.create_text_message("Failed to parse response from TempGuru API.")| def _invoke( | ||
| self, tool_parameters: dict[str, Any] | ||
| ) -> Generator[ToolInvokeMessage, None, None]: | ||
| params = {"state": tool_parameters["state"]} | ||
| response = requests.get( | ||
| f"{API_BASE}/compliance", | ||
| params=params, | ||
| headers={"User-Agent": USER_AGENT, "Accept": "application/json"}, | ||
| timeout=30, | ||
| ) | ||
| response.raise_for_status() | ||
| yield self.create_json_message(response.json()) |
There was a problem hiding this comment.
Safely retrieve the required 'state' parameter and wrap the HTTP request in a try-except block to handle potential network errors or parsing failures gracefully.
def _invoke(
self, tool_parameters: dict[str, Any]
) -> Generator[ToolInvokeMessage, None, None]:
state = tool_parameters.get("state")
if not state:
yield self.create_text_message("The 'state' parameter is required.")
return
params = {"state": state}
try:
response = requests.get(
f"{API_BASE}/compliance",
params=params,
headers={"User-Agent": USER_AGENT, "Accept": "application/json"},
timeout=30,
)
response.raise_for_status()
yield self.create_json_message(response.json())
except requests.RequestException as e:
yield self.create_text_message(f"API request failed: {str(e)}")
except ValueError:
yield self.create_text_message("Failed to parse response from TempGuru API.")| def _invoke( | ||
| self, tool_parameters: dict[str, Any] | ||
| ) -> Generator[ToolInvokeMessage, None, None]: | ||
| params = { | ||
| "role": tool_parameters["role"], | ||
| "city": tool_parameters["city"], | ||
| } | ||
| response = requests.get( | ||
| f"{API_BASE}/pricing", | ||
| params=params, | ||
| headers={"User-Agent": USER_AGENT, "Accept": "application/json"}, | ||
| timeout=30, | ||
| ) | ||
| response.raise_for_status() | ||
| yield self.create_json_message(response.json()) |
There was a problem hiding this comment.
Perform defensive parameter checks and wrap the HTTP request in a try-except block to handle network errors or invalid JSON responses gracefully.
def _invoke(
self, tool_parameters: dict[str, Any]
) -> Generator[ToolInvokeMessage, None, None]:
role = tool_parameters.get("role")
city = tool_parameters.get("city")
if not role or not city:
yield self.create_text_message("Both 'role' and 'city' parameters are required.")
return
params = {
"role": role,
"city": city,
}
try:
response = requests.get(
f"{API_BASE}/pricing",
params=params,
headers={"User-Agent": USER_AGENT, "Accept": "application/json"},
timeout=30,
)
response.raise_for_status()
yield self.create_json_message(response.json())
except requests.RequestException as e:
yield self.create_text_message(f"API request failed: {str(e)}")
except ValueError:
yield self.create_text_message("Failed to parse response from TempGuru API.")| def _invoke( | ||
| self, tool_parameters: dict[str, Any] | ||
| ) -> Generator[ToolInvokeMessage, None, None]: | ||
| response = requests.get( | ||
| f"{API_BASE}/roles", | ||
| headers={"User-Agent": USER_AGENT, "Accept": "application/json"}, | ||
| timeout=30, | ||
| ) | ||
| response.raise_for_status() | ||
| yield self.create_json_message(response.json()) |
There was a problem hiding this comment.
Wrap the HTTP request in a try-except block to handle network errors or invalid JSON responses gracefully.
| def _invoke( | |
| self, tool_parameters: dict[str, Any] | |
| ) -> Generator[ToolInvokeMessage, None, None]: | |
| response = requests.get( | |
| f"{API_BASE}/roles", | |
| headers={"User-Agent": USER_AGENT, "Accept": "application/json"}, | |
| timeout=30, | |
| ) | |
| response.raise_for_status() | |
| yield self.create_json_message(response.json()) | |
| def _invoke( | |
| self, tool_parameters: dict[str, Any] | |
| ) -> Generator[ToolInvokeMessage, None, None]: | |
| try: | |
| response = requests.get( | |
| f"{API_BASE}/roles", | |
| headers={"User-Agent": USER_AGENT, "Accept": "application/json"}, | |
| timeout=30, | |
| ) | |
| response.raise_for_status() | |
| yield self.create_json_message(response.json()) | |
| except requests.RequestException as e: | |
| yield self.create_text_message(f"API request failed: {str(e)}") | |
| except ValueError: | |
| yield self.create_text_message("Failed to parse response from TempGuru API.") |
…vider validation Applies the 7 suggestions from the Gemini Code Assist bot on PR langgenius#3252: provider/tempguru.py - Replace SDK-internal-method validation with a direct requests.get probe. Avoids coupling the validator to Tool subclass internals. - Remove unused GetCitiesTool import; add requests. All 5 tool handlers - Wrap requests.get in try/except for requests.RequestException and ValueError. Yield create_text_message on errors instead of raising. Tools with required parameters - Use tool_parameters.get() instead of subscript access on required params. Return early with a helpful error message if any required param is missing.
|
Thanks for the thorough review. All 7 suggestions applied in provider/tempguru.py:
All 5 tool handlers:
Tools with required params (
Ready for re-review. |
CI's astral-sh/setup-uv@v6 looks for uv.lock or requirements.txt to invalidate the dependency cache. Without either, the cache key check fails with 'No file matched to .../uv.lock,.../requirements.txt' and the downstream test step exits 1. Generated by 'uv lock' against pyproject.toml. Resolved 41 packages including dify_plugin transitive deps. Lockfile is portable across Python 3.12+ per requires-python in pyproject.toml.
The pre-pr-check-per-plugin workflow at .github/workflows/ line 120 hard-requires the top-level manifest.yaml author field to match 'langgenius' (the org maintaining the official-plugins monorepo). Individual contributor attribution stays in provider/<name>.yaml which retains 'author: tempguru' — same pattern as duckduckgo (provider author: yash_parmar) and other community contributions.
Summary
Adds the
tempgurutool plugin undertools/tempguru/. Wraps the public TempGuru MCP REST API as 5 read-only Dify tools so any Dify agent can answer event-staffing questions (coverage, rates, lead times, state compliance) for events across 300+ US and Canadian markets — no authentication, no API key, no setup.Matches the no-auth precedent set by
duckduckgoandwikipediain the same plugins directory.What it does
get_citiesget_rolescheck_availabilityget_role_pricingget_compliance_by_stateEach tool is a ~20-line Python class that makes one HTTPS
GETtomcp.tempguru.co/api/v1/<endpoint>and yields the response JSON back to Dify. No business logic reimplemented; the TempGuru MCP REST mirror is the canonical source.Trust profile
GET. NoPOST/PUT/DELETEexists in the plugin or the underlying API.Bilingual labels
All
label,human_description, anddescription.humanfields ship in bothen_USandzh_Hans(Simplified Chinese), matching the convention of the highest-quality existing plugins. TempGuru already serves a Chinese-facing agent page at https://tempguru.co/zh-cn/ai.Already shipped on adjacent surfaces
co.tempguru/event-staffing(DNS-verified namespace)tempguru/event-staffing(quality score 90/100)tempguru/TempGuru-Event-StaffingDocumentation
License
MIT