From 9f45220abb7408aeefb0cf0b255c0bd7c2ac5651 Mon Sep 17 00:00:00 2001 From: Danny Roosevelt Date: Tue, 27 May 2025 16:46:56 -0700 Subject: [PATCH] Adding app selector to MCP OpenAI docs --- docs-v2/components/AppSearchDemo.jsx | 237 +++++++++++++++++++++++++ docs-v2/next.config.mjs | 8 + docs-v2/pages/api/demo-connect/apps.js | 83 +++++++++ docs-v2/pages/connect/mcp/openai.mdx | 4 +- pnpm-lock.yaml | 2 + 5 files changed, 332 insertions(+), 2 deletions(-) create mode 100644 docs-v2/components/AppSearchDemo.jsx create mode 100644 docs-v2/pages/api/demo-connect/apps.js diff --git a/docs-v2/components/AppSearchDemo.jsx b/docs-v2/components/AppSearchDemo.jsx new file mode 100644 index 0000000000000..14dde93f93fe8 --- /dev/null +++ b/docs-v2/components/AppSearchDemo.jsx @@ -0,0 +1,237 @@ +"use client"; + +import { + useState, useEffect, useCallback, +} from "react"; +import { styles } from "../utils/componentStyles"; +import { generateRequestToken } from "./api"; + +// Debounce hook +function useDebounce(value, delay) { + const [ + debouncedValue, + setDebouncedValue, + ] = useState(value); + + useEffect(() => { + const handler = setTimeout(() => { + setDebouncedValue(value); + }, delay); + + return () => { + clearTimeout(handler); + }; + }, [ + value, + delay, + ]); + + return debouncedValue; +} + +export default function AppSearchDemo() { + const [ + searchQuery, + setSearchQuery, + ] = useState(""); + const [ + apps, + setApps, + ] = useState([]); + const [ + isLoading, + setIsLoading, + ] = useState(false); + const [ + error, + setError, + ] = useState(""); + const [ + copiedSlug, + setCopiedSlug, + ] = useState(""); + + const debouncedSearchQuery = useDebounce(searchQuery, 300); + + const searchApps = useCallback(async (query) => { + if (!query || query.length < 2) { + setApps([]); + return; + } + + setIsLoading(true); + setError(""); + + try { + const requestToken = generateRequestToken(); + // Convert spaces to underscores for name_slug searching + const searchQuery = query.replace(/\s+/g, "_"); + const response = await fetch( + `/docs/api-demo-connect/apps?q=${encodeURIComponent(searchQuery)}&limit=5`, + { + headers: { + "Content-Type": "application/json", + "X-Request-Token": requestToken, + }, + }, + ); + + if (!response.ok) { + throw new Error("Failed to search apps"); + } + + const data = await response.json(); + console.log("App icons:", data.apps.map((app) => ({ + name: app.name, + icon: app.icon, + }))); + setApps(data.apps); + } catch (err) { + console.error("Error searching apps:", err); + setError("Failed to search apps. Please try again."); + setApps([]); + } finally { + setIsLoading(false); + } + }, []); + + useEffect(() => { + searchApps(debouncedSearchQuery); + }, [ + debouncedSearchQuery, + searchApps, + ]); + + async function copyToClipboard(nameSlug) { + try { + await navigator.clipboard.writeText(nameSlug); + setCopiedSlug(nameSlug); + setTimeout(() => setCopiedSlug(""), 2000); + } catch (err) { + console.error("Failed to copy:", err); + } + } + + return ( +
+
Search for an app
+
+ setSearchQuery(e.target.value)} + placeholder="Search for an app (e.g., slack, notion, gmail)" + className="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-800 text-gray-800 dark:text-gray-200 placeholder-gray-400 dark:placeholder-gray-500" + /> + + {searchQuery.length > 0 && searchQuery.length < 2 && ( +

+ Type at least 2 characters to search +

+ )} + + {isLoading && ( +
+

Searching...

+
+ )} + + {error && ( +
+

{error}

+
+ )} + + {apps.length > 0 && !isLoading && ( +
+ {apps.map((app) => ( +
+
+ {app.icon && ( + {app.name} + )} +
+
+

+ {app.name} +

+ + {app.name_slug} + + +
+

+ {app.description} +

+ {app.categories.length > 0 && ( +
+ {app.categories.map((category) => ( + + {category} + + ))} +
+ )} +
+
+
+ ))} +
+ )} + + {debouncedSearchQuery.length >= 2 && + apps.length === 0 && + !isLoading && + !error && ( +
+

+ No apps found for "{debouncedSearchQuery}" +

+
+ )} + +
+

+ Browse all available apps at{" "} + + mcp.pipedream.com + +

+
+
+
+ ); +} diff --git a/docs-v2/next.config.mjs b/docs-v2/next.config.mjs index 77b7465d95248..03252cc5d301f 100644 --- a/docs-v2/next.config.mjs +++ b/docs-v2/next.config.mjs @@ -581,6 +581,14 @@ export default withNextra({ source: "/api-demo-connect/accounts/:id/", destination: "/api/demo-connect/accounts/:id", }, + { + source: "/api-demo-connect/apps", + destination: "/api/demo-connect/apps", + }, + { + source: "/api-demo-connect/apps/", + destination: "/api/demo-connect/apps", + }, { source: "/workflows/errors/", destination: "/workflows/building-workflows/errors/", diff --git a/docs-v2/pages/api/demo-connect/apps.js b/docs-v2/pages/api/demo-connect/apps.js new file mode 100644 index 0000000000000..41c90f7711522 --- /dev/null +++ b/docs-v2/pages/api/demo-connect/apps.js @@ -0,0 +1,83 @@ +// Search for apps in the Pipedream API + +import { createApiHandler } from "./utils"; + +/** + * Handler for searching apps + */ +async function appsHandler(req, res) { + try { + const { + q, limit = 50, + } = req.query; + + // Build the query parameters + const params = new URLSearchParams(); + if (q) params.append("q", q); + params.append("limit", String(limit)); + params.append("has_actions", "1"); // Only apps with components + + // First get an OAuth token + const tokenResponse = await fetch( + "https://api.pipedream.com/v1/oauth/token", + { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + grant_type: "client_credentials", + client_id: process.env.PIPEDREAM_CLIENT_ID, + client_secret: process.env.PIPEDREAM_CLIENT_SECRET, + }), + }, + ); + + if (!tokenResponse.ok) { + throw new Error("Failed to authenticate"); + } + + const { access_token } = await tokenResponse.json(); + + // Now search for apps + const appsResponse = await fetch( + `https://api.pipedream.com/v1/apps?${params.toString()}`, + { + headers: { + "Authorization": `Bearer ${access_token}`, + "Content-Type": "application/json", + }, + }, + ); + + if (!appsResponse.ok) { + throw new Error("Failed to fetch apps"); + } + + const appsData = await appsResponse.json(); + + // Format the response with the fields we need + const formattedApps = appsData.data.map((app) => ({ + id: app.id, + name: app.name, + name_slug: app.name_slug, + description: app.description, + icon: app.img_src, + categories: app.categories || [], + })); + + return res.status(200).json({ + apps: formattedApps, + total_count: appsData.page_info?.total_count || formattedApps.length, + }); + } catch (error) { + console.error("Error searching apps:", error); + return res.status(500).json({ + error: "Failed to search apps", + details: error.message, + }); + } +} + +// Export the handler wrapped with security checks +export default createApiHandler(appsHandler, "GET"); diff --git a/docs-v2/pages/connect/mcp/openai.mdx b/docs-v2/pages/connect/mcp/openai.mdx index 7d868b8dab72c..72cc5399a50cf 100644 --- a/docs-v2/pages/connect/mcp/openai.mdx +++ b/docs-v2/pages/connect/mcp/openai.mdx @@ -1,5 +1,6 @@ import { Callout, Tabs, Steps } from 'nextra/components' import TemporaryTokenGenerator from '@/components/TemporaryTokenGenerator' +import AppSearchDemo from '@/components/AppSearchDemo' # Using Pipedream MCP with OpenAI @@ -29,8 +30,7 @@ Click the **Create** button in the **Tools** section, then select **Pipedream**. #### Select an app -- Select an app you want to use as an MCP server. For example, `notion`, `google_calendar`, `gmail`, or `slack`. -- Check out all of the available apps here: [mcp.pipedream.com](https://mcp.pipedream.com). + #### Click **Connect** diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d4d7d02b179cc..c4d16263bd210 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -35897,6 +35897,8 @@ snapshots: '@putout/operator-filesystem': 5.0.0(putout@36.13.1(eslint@8.57.1)(typescript@5.6.3)) '@putout/operator-json': 2.2.0 putout: 36.13.1(eslint@8.57.1)(typescript@5.6.3) + transitivePeerDependencies: + - supports-color '@putout/operator-regexp@1.0.0(putout@36.13.1(eslint@8.57.1)(typescript@5.6.3))': dependencies: