Skip to content

Commit 9f45220

Browse files
Adding app selector to MCP OpenAI docs
1 parent ed71f9b commit 9f45220

File tree

5 files changed

+332
-2
lines changed

5 files changed

+332
-2
lines changed
Lines changed: 237 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,237 @@
1+
"use client";
2+
3+
import {
4+
useState, useEffect, useCallback,
5+
} from "react";
6+
import { styles } from "../utils/componentStyles";
7+
import { generateRequestToken } from "./api";
8+
9+
// Debounce hook
10+
function useDebounce(value, delay) {
11+
const [
12+
debouncedValue,
13+
setDebouncedValue,
14+
] = useState(value);
15+
16+
useEffect(() => {
17+
const handler = setTimeout(() => {
18+
setDebouncedValue(value);
19+
}, delay);
20+
21+
return () => {
22+
clearTimeout(handler);
23+
};
24+
}, [
25+
value,
26+
delay,
27+
]);
28+
29+
return debouncedValue;
30+
}
31+
32+
export default function AppSearchDemo() {
33+
const [
34+
searchQuery,
35+
setSearchQuery,
36+
] = useState("");
37+
const [
38+
apps,
39+
setApps,
40+
] = useState([]);
41+
const [
42+
isLoading,
43+
setIsLoading,
44+
] = useState(false);
45+
const [
46+
error,
47+
setError,
48+
] = useState("");
49+
const [
50+
copiedSlug,
51+
setCopiedSlug,
52+
] = useState("");
53+
54+
const debouncedSearchQuery = useDebounce(searchQuery, 300);
55+
56+
const searchApps = useCallback(async (query) => {
57+
if (!query || query.length < 2) {
58+
setApps([]);
59+
return;
60+
}
61+
62+
setIsLoading(true);
63+
setError("");
64+
65+
try {
66+
const requestToken = generateRequestToken();
67+
// Convert spaces to underscores for name_slug searching
68+
const searchQuery = query.replace(/\s+/g, "_");
69+
const response = await fetch(
70+
`/docs/api-demo-connect/apps?q=${encodeURIComponent(searchQuery)}&limit=5`,
71+
{
72+
headers: {
73+
"Content-Type": "application/json",
74+
"X-Request-Token": requestToken,
75+
},
76+
},
77+
);
78+
79+
if (!response.ok) {
80+
throw new Error("Failed to search apps");
81+
}
82+
83+
const data = await response.json();
84+
console.log("App icons:", data.apps.map((app) => ({
85+
name: app.name,
86+
icon: app.icon,
87+
})));
88+
setApps(data.apps);
89+
} catch (err) {
90+
console.error("Error searching apps:", err);
91+
setError("Failed to search apps. Please try again.");
92+
setApps([]);
93+
} finally {
94+
setIsLoading(false);
95+
}
96+
}, []);
97+
98+
useEffect(() => {
99+
searchApps(debouncedSearchQuery);
100+
}, [
101+
debouncedSearchQuery,
102+
searchApps,
103+
]);
104+
105+
async function copyToClipboard(nameSlug) {
106+
try {
107+
await navigator.clipboard.writeText(nameSlug);
108+
setCopiedSlug(nameSlug);
109+
setTimeout(() => setCopiedSlug(""), 2000);
110+
} catch (err) {
111+
console.error("Failed to copy:", err);
112+
}
113+
}
114+
115+
return (
116+
<div className={styles.container}>
117+
<div className={styles.header}>Search for an app</div>
118+
<div className="p-4">
119+
<input
120+
type="text"
121+
value={searchQuery}
122+
onChange={(e) => setSearchQuery(e.target.value)}
123+
placeholder="Search for an app (e.g., slack, notion, gmail)"
124+
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"
125+
/>
126+
127+
{searchQuery.length > 0 && searchQuery.length < 2 && (
128+
<p className={styles.text.muted + " mt-2"}>
129+
Type at least 2 characters to search
130+
</p>
131+
)}
132+
133+
{isLoading && (
134+
<div className="mt-4 text-center">
135+
<p className={styles.text.normal}>Searching...</p>
136+
</div>
137+
)}
138+
139+
{error && (
140+
<div className={styles.statusBox.error}>
141+
<p className="text-sm">{error}</p>
142+
</div>
143+
)}
144+
145+
{apps.length > 0 && !isLoading && (
146+
<div className="mt-4 space-y-3">
147+
{apps.map((app) => (
148+
<div
149+
key={app.id}
150+
className="p-4 border border-gray-200 dark:border-gray-700 rounded-lg hover:border-gray-300 dark:hover:border-gray-600 transition-colors"
151+
>
152+
<div className="flex items-start gap-3">
153+
{app.icon && (
154+
<img
155+
src={app.icon}
156+
alt={app.name}
157+
className="w-10 h-10 rounded"
158+
/>
159+
)}
160+
<div className="flex-1">
161+
<div className="flex items-center gap-2 mb-1 flex-nowrap">
162+
<p className="font-semibold text-base text-gray-800 dark:text-gray-200 flex-shrink-0 m-0">
163+
{app.name}
164+
</p>
165+
<code className="text-sm px-2 py-0.5 bg-gray-100 dark:bg-gray-800 text-gray-600 dark:text-gray-400 rounded flex-shrink-0">
166+
{app.name_slug}
167+
</code>
168+
<button
169+
onClick={() => copyToClipboard(app.name_slug)}
170+
className="p-1 hover:bg-gray-100 dark:hover:bg-gray-700 rounded transition-colors flex-shrink-0"
171+
title={copiedSlug === app.name_slug
172+
? "Copied!"
173+
: "Copy app name slug"}
174+
>
175+
{copiedSlug === app.name_slug
176+
? (
177+
<svg className="w-4 h-4 text-green-600 dark:text-green-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
178+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
179+
</svg>
180+
)
181+
: (
182+
<svg className="w-4 h-4 text-gray-500 dark:text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
183+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
184+
</svg>
185+
)}
186+
</button>
187+
</div>
188+
<p className={styles.text.muted + " line-clamp-2"}>
189+
{app.description}
190+
</p>
191+
{app.categories.length > 0 && (
192+
<div className="mt-2 flex flex-wrap gap-1">
193+
{app.categories.map((category) => (
194+
<span
195+
key={category}
196+
className="text-xs px-2 py-1 bg-gray-100 dark:bg-gray-800 text-gray-600 dark:text-gray-400 rounded"
197+
>
198+
{category}
199+
</span>
200+
))}
201+
</div>
202+
)}
203+
</div>
204+
</div>
205+
</div>
206+
))}
207+
</div>
208+
)}
209+
210+
{debouncedSearchQuery.length >= 2 &&
211+
apps.length === 0 &&
212+
!isLoading &&
213+
!error && (
214+
<div className="mt-4 text-center">
215+
<p className={styles.text.muted}>
216+
No apps found for "{debouncedSearchQuery}"
217+
</p>
218+
</div>
219+
)}
220+
221+
<div className="mt-4">
222+
<p className={styles.text.small}>
223+
Browse all available apps at{" "}
224+
<a
225+
href="https://mcp.pipedream.com"
226+
target="_blank"
227+
rel="noopener noreferrer"
228+
className="text-blue-500 hover:text-blue-600"
229+
>
230+
mcp.pipedream.com
231+
</a>
232+
</p>
233+
</div>
234+
</div>
235+
</div>
236+
);
237+
}

docs-v2/next.config.mjs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -581,6 +581,14 @@ export default withNextra({
581581
source: "/api-demo-connect/accounts/:id/",
582582
destination: "/api/demo-connect/accounts/:id",
583583
},
584+
{
585+
source: "/api-demo-connect/apps",
586+
destination: "/api/demo-connect/apps",
587+
},
588+
{
589+
source: "/api-demo-connect/apps/",
590+
destination: "/api/demo-connect/apps",
591+
},
584592
{
585593
source: "/workflows/errors/",
586594
destination: "/workflows/building-workflows/errors/",
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
// Search for apps in the Pipedream API
2+
3+
import { createApiHandler } from "./utils";
4+
5+
/**
6+
* Handler for searching apps
7+
*/
8+
async function appsHandler(req, res) {
9+
try {
10+
const {
11+
q, limit = 50,
12+
} = req.query;
13+
14+
// Build the query parameters
15+
const params = new URLSearchParams();
16+
if (q) params.append("q", q);
17+
params.append("limit", String(limit));
18+
params.append("has_actions", "1"); // Only apps with components
19+
20+
// First get an OAuth token
21+
const tokenResponse = await fetch(
22+
"https://api.pipedream.com/v1/oauth/token",
23+
{
24+
method: "POST",
25+
headers: {
26+
"Content-Type": "application/json",
27+
},
28+
body: JSON.stringify({
29+
grant_type: "client_credentials",
30+
client_id: process.env.PIPEDREAM_CLIENT_ID,
31+
client_secret: process.env.PIPEDREAM_CLIENT_SECRET,
32+
}),
33+
},
34+
);
35+
36+
if (!tokenResponse.ok) {
37+
throw new Error("Failed to authenticate");
38+
}
39+
40+
const { access_token } = await tokenResponse.json();
41+
42+
// Now search for apps
43+
const appsResponse = await fetch(
44+
`https://api.pipedream.com/v1/apps?${params.toString()}`,
45+
{
46+
headers: {
47+
"Authorization": `Bearer ${access_token}`,
48+
"Content-Type": "application/json",
49+
},
50+
},
51+
);
52+
53+
if (!appsResponse.ok) {
54+
throw new Error("Failed to fetch apps");
55+
}
56+
57+
const appsData = await appsResponse.json();
58+
59+
// Format the response with the fields we need
60+
const formattedApps = appsData.data.map((app) => ({
61+
id: app.id,
62+
name: app.name,
63+
name_slug: app.name_slug,
64+
description: app.description,
65+
icon: app.img_src,
66+
categories: app.categories || [],
67+
}));
68+
69+
return res.status(200).json({
70+
apps: formattedApps,
71+
total_count: appsData.page_info?.total_count || formattedApps.length,
72+
});
73+
} catch (error) {
74+
console.error("Error searching apps:", error);
75+
return res.status(500).json({
76+
error: "Failed to search apps",
77+
details: error.message,
78+
});
79+
}
80+
}
81+
82+
// Export the handler wrapped with security checks
83+
export default createApiHandler(appsHandler, "GET");

docs-v2/pages/connect/mcp/openai.mdx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { Callout, Tabs, Steps } from 'nextra/components'
22
import TemporaryTokenGenerator from '@/components/TemporaryTokenGenerator'
3+
import AppSearchDemo from '@/components/AppSearchDemo'
34

45
# Using Pipedream MCP with OpenAI
56

@@ -29,8 +30,7 @@ Click the **Create** button in the **Tools** section, then select **Pipedream**.
2930

3031
#### Select an app
3132

32-
- Select an app you want to use as an MCP server. For example, `notion`, `google_calendar`, `gmail`, or `slack`.
33-
- Check out all of the available apps here: [mcp.pipedream.com](https://mcp.pipedream.com).
33+
<AppSearchDemo />
3434

3535
#### Click **Connect**
3636

pnpm-lock.yaml

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)