@decocms/mesh-sdk 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,368 @@
1
+ # @decocms/mesh-sdk
2
+
3
+ SDK for building external apps that integrate with Mesh MCP servers. Provides React hooks and utilities for managing connections, authenticating with OAuth, and calling MCP tools.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @decocms/mesh-sdk @decocms/bindings
9
+ # or
10
+ bun add @decocms/mesh-sdk @decocms/bindings
11
+ ```
12
+
13
+ ### Peer Dependencies
14
+
15
+ ```bash
16
+ npm install react @tanstack/react-query
17
+ # Optional: for toast notifications
18
+ npm install sonner
19
+ ```
20
+
21
+ ## Quick Start
22
+
23
+ ### 1. Create an API Key
24
+
25
+ In Mesh, call the `API_KEY_CREATE` tool to create an API key with the appropriate scopes for the connections you want to access. The API key will be used to authenticate your external app.
26
+
27
+ ```typescript
28
+ // Example: Create an API key via MCP
29
+ await client.callTool({
30
+ name: "API_KEY_CREATE",
31
+ arguments: {
32
+ name: "My External App",
33
+ scopes: ["connections:read", "connections:write"],
34
+ },
35
+ });
36
+ ```
37
+
38
+ ### 2. Server-Side: Connect to Mesh
39
+
40
+ ```typescript
41
+ // server.ts (Node.js / Bun / your backend)
42
+ import { createMCPClient } from "@decocms/mesh-sdk";
43
+
44
+ // Create an MCP client - keep API key on server only!
45
+ const client = await createMCPClient({
46
+ meshUrl: "https://mesh.your-company.com", // Your Mesh server URL
47
+ connectionId: "self", // "self" for management API
48
+ orgId: "org_xxxxx", // Your organization ID
49
+ token: process.env.MESH_API_KEY, // API key from environment
50
+ });
51
+
52
+ // List connections
53
+ const result = await client.callTool({
54
+ name: "COLLECTION_CONNECTIONS_LIST",
55
+ arguments: { limit: 100 },
56
+ });
57
+ ```
58
+
59
+ ### 3. Client-Side: Set Up React App
60
+
61
+ ```tsx
62
+ // app.tsx (React client)
63
+ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
64
+ import { Toaster } from "sonner";
65
+
66
+ const queryClient = new QueryClient();
67
+
68
+ function App() {
69
+ return (
70
+ <QueryClientProvider client={queryClient}>
71
+ <YourApp />
72
+ <Toaster />
73
+ </QueryClientProvider>
74
+ );
75
+ }
76
+ ```
77
+
78
+ ## Full Example: User Sandbox Integration
79
+
80
+ The most common use case for external apps is letting your end-users connect their own MCPs via the **User Sandbox plugin**. This creates isolated connections per user.
81
+
82
+ ### Server-Side: Create a Connect Session
83
+
84
+ ```typescript
85
+ // server/api.ts (Hono / Express / your backend)
86
+ import { Hono } from "hono";
87
+ import { createMCPClient } from "@decocms/mesh-sdk";
88
+
89
+ const app = new Hono();
90
+
91
+ const MESH_URL = process.env.MESH_URL!;
92
+ const ORG_ID = process.env.MESH_ORG_ID!;
93
+ const API_KEY = process.env.MESH_API_KEY!;
94
+
95
+ let meshClient: Awaited<ReturnType<typeof createMCPClient>> | null = null;
96
+
97
+ async function getMeshClient() {
98
+ if (!meshClient) {
99
+ meshClient = await createMCPClient({
100
+ meshUrl: MESH_URL,
101
+ connectionId: "self",
102
+ orgId: ORG_ID,
103
+ token: API_KEY,
104
+ });
105
+ }
106
+ return meshClient;
107
+ }
108
+
109
+ // POST /api/connect-session - Create a session for end-user to connect MCPs
110
+ app.post("/api/connect-session", async (c) => {
111
+ const { userId } = await c.req.json();
112
+ const client = await getMeshClient();
113
+
114
+ // Call User Sandbox plugin to create a connect session
115
+ const result = await client.callTool({
116
+ name: "USER_SANDBOX_CREATE_SESSION",
117
+ arguments: {
118
+ templateId: "your-template-id", // Created in Mesh dashboard
119
+ externalUserId: userId, // Your app's user ID
120
+ redirectUrl: "https://your-app.com/connect/complete",
121
+ },
122
+ });
123
+
124
+ const payload = result as { structuredContent?: { sessionUrl: string } };
125
+ return c.json({ connectUrl: payload.structuredContent?.sessionUrl });
126
+ });
127
+
128
+ export default app;
129
+ ```
130
+
131
+ ### Client-Side: Redirect User to Connect
132
+
133
+ ```tsx
134
+ // client/connect-button.tsx (React)
135
+
136
+ function ConnectIntegrationsButton({ userId }: { userId: string }) {
137
+ const [isLoading, setIsLoading] = useState(false);
138
+
139
+ const handleConnect = async () => {
140
+ setIsLoading(true);
141
+
142
+ // Get connect session URL from your server
143
+ const res = await fetch("/api/connect-session", {
144
+ method: "POST",
145
+ headers: { "Content-Type": "application/json" },
146
+ body: JSON.stringify({ userId }),
147
+ });
148
+ const { connectUrl } = await res.json();
149
+
150
+ // Redirect user to Mesh connect flow
151
+ window.location.href = connectUrl;
152
+ };
153
+
154
+ return (
155
+ <button
156
+ onClick={handleConnect}
157
+ disabled={isLoading}
158
+ className="px-4 py-2 bg-blue-600 text-white rounded"
159
+ >
160
+ {isLoading ? "Loading..." : "Connect Your Apps"}
161
+ </button>
162
+ );
163
+ }
164
+ ```
165
+
166
+ ## Example: Direct MCP Tool Calls
167
+
168
+ For server-side automation or admin tasks, you can call any MCP tool directly:
169
+
170
+ ```typescript
171
+ // server-side only
172
+ import { createMCPClient } from "@decocms/mesh-sdk";
173
+
174
+ const client = await createMCPClient({
175
+ meshUrl: "https://mesh.your-company.com",
176
+ connectionId: "self",
177
+ orgId: process.env.ORG_ID!,
178
+ token: process.env.MESH_API_KEY!,
179
+ });
180
+
181
+ // List Virtual MCPs (agents)
182
+ const agents = await client.callTool({
183
+ name: "COLLECTION_VIRTUAL_MCP_LIST",
184
+ arguments: { limit: 100 },
185
+ });
186
+
187
+ // Create a connection
188
+ const newConn = await client.callTool({
189
+ name: "COLLECTION_CONNECTIONS_CREATE",
190
+ arguments: {
191
+ data: {
192
+ title: "My MCP Server",
193
+ url: "https://mcp.example.com/sse",
194
+ },
195
+ },
196
+ });
197
+
198
+ // Call a tool on a specific connection
199
+ const specificClient = await createMCPClient({
200
+ meshUrl: "https://mesh.your-company.com",
201
+ connectionId: "conn_xxx", // Target connection ID
202
+ orgId: process.env.ORG_ID!,
203
+ token: process.env.MESH_API_KEY!,
204
+ });
205
+
206
+ const result = await specificClient.callTool({
207
+ name: "SOME_TOOL_ON_THAT_MCP",
208
+ arguments: { /* ... */ },
209
+ });
210
+ ```
211
+
212
+ ## API Reference
213
+
214
+ ### `createMCPClient(options)` - Server-Side
215
+
216
+ Creates and connects an MCP client to a Mesh server. **Use on server only** - don't expose your API key to the client.
217
+
218
+ ```typescript
219
+ // server-side only
220
+ const client = await createMCPClient({
221
+ meshUrl: "https://mesh.example.com", // Required for external apps
222
+ connectionId: "self", // "self" for management API, or connection ID
223
+ orgId: "org_xxx", // Organization ID
224
+ token: process.env.MESH_API_KEY, // API key from environment
225
+ });
226
+ ```
227
+
228
+ ### `useMCPClient(options)` - Client-Side (Same-Origin Only)
229
+
230
+ React hook version of `createMCPClient`. Uses Suspense. **Only use when running on the same origin as Mesh** (e.g., inside the Mesh app itself).
231
+
232
+ ```typescript
233
+ // client-side - only for same-origin apps
234
+ function MyComponent() {
235
+ const client = useMCPClient({
236
+ connectionId: "self",
237
+ orgId: "org_xxx",
238
+ // No token needed when using cookies on same origin
239
+ });
240
+
241
+ // client is ready to use
242
+ }
243
+ ```
244
+
245
+ ### `authenticateMcp(options)` - Client-Side
246
+
247
+ Triggers OAuth authentication flow for an MCP connection. **This runs client-side** - it doesn't expose your API key.
248
+
249
+ ```typescript
250
+ // client-side - safe to use in browser
251
+ const result = await authenticateMcp({
252
+ meshUrl: "https://mesh.example.com", // Required for external apps
253
+ connectionId: "conn_xxx", // Connection to authenticate
254
+ callbackUrl: "https://your-app.com/oauth/callback", // Your OAuth callback URL
255
+ timeout: 120000, // Timeout in ms (default: 120000)
256
+ scope: ["read", "write"], // OAuth scopes (optional)
257
+ windowMode: "popup", // "popup" (default) or "tab"
258
+ });
259
+
260
+ if (result.error) {
261
+ console.error("Auth failed:", result.error);
262
+ } else {
263
+ console.log("Got token:", result.token);
264
+ }
265
+ ```
266
+
267
+ **Window modes:**
268
+ - `"popup"` (default): Opens OAuth in a popup window. May be blocked on some mobile devices.
269
+ - `"tab"`: Opens OAuth in a new tab. Works on all devices. Uses localStorage for cross-tab communication.
270
+
271
+ ### `isConnectionAuthenticated(options)` - Server or Client
272
+
273
+ Check if a connection is authenticated. Can be used on either server or client.
274
+
275
+ ```typescript
276
+ const status = await isConnectionAuthenticated({
277
+ url: "https://mesh.example.com/mcp/conn_xxx",
278
+ token: "bearer_token", // Optional
279
+ meshUrl: "https://mesh.example.com", // For API calls
280
+ });
281
+
282
+ console.log(status.isAuthenticated); // boolean
283
+ console.log(status.supportsOAuth); // boolean
284
+ console.log(status.hasOAuthToken); // boolean
285
+ ```
286
+
287
+ ### Collection Hooks - Client-Side (Same-Origin Only)
288
+
289
+ When using with `ProjectContextProvider`, you get access to collection hooks. **Only use when running on the same origin as Mesh** (e.g., inside the Mesh app or plugins).
290
+
291
+ ```typescript
292
+ // client-side - only for same-origin apps (inside Mesh)
293
+ import {
294
+ ProjectContextProvider,
295
+ useConnections,
296
+ useConnection,
297
+ useConnectionActions,
298
+ } from "@decocms/mesh-sdk";
299
+
300
+ function App() {
301
+ return (
302
+ <ProjectContextProvider
303
+ org={{ id: "org_xxx", slug: "my-org", name: "My Org", logo: null }}
304
+ project={{ slug: "org-admin" }}
305
+ >
306
+ <ConnectionsManager />
307
+ </ProjectContextProvider>
308
+ );
309
+ }
310
+
311
+ function ConnectionsManager() {
312
+ // List all connections
313
+ const connections = useConnections();
314
+
315
+ // Get single connection
316
+ const connection = useConnection("conn_xxx");
317
+
318
+ // CRUD actions
319
+ const { create, update, delete: remove } = useConnectionActions();
320
+
321
+ await create.mutateAsync({
322
+ title: "My MCP",
323
+ url: "https://mcp.example.com",
324
+ });
325
+ }
326
+ ```
327
+
328
+ ## OAuth Callback Setup - Client-Side
329
+
330
+ If you're using `authenticateMcp()` directly (not via User Sandbox plugin), you need an OAuth callback route. This is a **client-side page** that receives the OAuth authorization code:
331
+
332
+ ```tsx
333
+ // pages/oauth/callback.tsx (React client-side page)
334
+ export function OAuthCallback() {
335
+ const params = new URLSearchParams(window.location.search);
336
+
337
+ if (window.opener) {
338
+ window.opener.postMessage({
339
+ type: "mcp:oauth:callback",
340
+ success: !params.get("error"),
341
+ code: params.get("code"),
342
+ state: params.get("state"),
343
+ error: params.get("error"),
344
+ }, window.location.origin);
345
+ }
346
+
347
+ return <p>Authentication complete. You can close this window.</p>;
348
+ }
349
+ ```
350
+
351
+ > **Note**: If you're using the User Sandbox plugin, OAuth is handled automatically in the connect flow - you don't need this callback page.
352
+
353
+ ## Types
354
+
355
+ ```typescript
356
+ import type {
357
+ ConnectionEntity,
358
+ ConnectionCreateData,
359
+ ConnectionUpdateData,
360
+ VirtualMCPEntity,
361
+ VirtualMCPCreateData,
362
+ VirtualMCPUpdateData,
363
+ } from "@decocms/mesh-sdk";
364
+ ```
365
+
366
+ ## License
367
+
368
+ MIT
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "@decocms/mesh-sdk",
3
+ "version": "1.1.0",
4
+ "description": "SDK for building external apps that integrate with Mesh MCP servers",
5
+ "type": "module",
6
+ "scripts": {
7
+ "check": "tsc --noEmit",
8
+ "test": "bun test"
9
+ },
10
+ "exports": {
11
+ ".": "./src/index.ts",
12
+ "./hooks": "./src/hooks/index.ts",
13
+ "./context": "./src/context/index.ts",
14
+ "./types": "./src/types/index.ts"
15
+ },
16
+ "dependencies": {
17
+ "@decocms/bindings": "^1.1.1",
18
+ "@modelcontextprotocol/sdk": "1.25.2",
19
+ "zod": "^4.0.0"
20
+ },
21
+ "peerDependencies": {
22
+ "@tanstack/react-query": ">=5.0.0",
23
+ "react": ">=18.0.0",
24
+ "sonner": ">=2.0.0"
25
+ },
26
+ "peerDependenciesMeta": {
27
+ "sonner": {
28
+ "optional": true
29
+ }
30
+ },
31
+ "engines": {
32
+ "node": ">=18.0.0"
33
+ },
34
+ "keywords": [
35
+ "mesh",
36
+ "mcp",
37
+ "model-context-protocol",
38
+ "sdk",
39
+ "react",
40
+ "hooks"
41
+ ],
42
+ "repository": {
43
+ "type": "git",
44
+ "url": "git+https://github.com/decocms/admin.git",
45
+ "directory": "packages/mesh-sdk"
46
+ },
47
+ "license": "MIT",
48
+ "publishConfig": {
49
+ "access": "public"
50
+ }
51
+ }
@@ -0,0 +1,9 @@
1
+ export {
2
+ ProjectContextProvider,
3
+ useProjectContext,
4
+ Locator,
5
+ ORG_ADMIN_PROJECT_SLUG,
6
+ type ProjectContextProviderProps,
7
+ type ProjectLocator,
8
+ type LocatorStructured,
9
+ } from "./project-context";
@@ -0,0 +1,89 @@
1
+ import { createContext, useContext, type PropsWithChildren } from "react";
2
+
3
+ /**
4
+ * a ProjectLocator is a github-like slug string that identifies a project in an organization.
5
+ *
6
+ * format: <org-slug>/<project-slug>
7
+ */
8
+ export type ProjectLocator = `${string}/${string}`;
9
+
10
+ export type LocatorStructured = {
11
+ org: string;
12
+ project: string;
13
+ };
14
+
15
+ export const ORG_ADMIN_PROJECT_SLUG = "org-admin";
16
+
17
+ export const Locator = {
18
+ from({ org, project }: LocatorStructured): ProjectLocator {
19
+ if (org?.includes("/") || project.includes("/")) {
20
+ throw new Error("Org or project cannot contain slashes");
21
+ }
22
+
23
+ return `${org}/${project}` as ProjectLocator;
24
+ },
25
+ parse(locator: ProjectLocator): LocatorStructured {
26
+ if (locator.startsWith("/")) {
27
+ locator = locator.slice(1) as ProjectLocator;
28
+ }
29
+ const [org, project] = locator.split("/");
30
+ if (!org || !project) {
31
+ throw new Error("Invalid locator");
32
+ }
33
+ return { org, project };
34
+ },
35
+ isOrgAdminProject(locator: ProjectLocator): boolean {
36
+ return locator.split("/")[1] === ORG_ADMIN_PROJECT_SLUG;
37
+ },
38
+ adminProject(org: string): ProjectLocator {
39
+ return `${org}/${ORG_ADMIN_PROJECT_SLUG}`;
40
+ },
41
+ } as const;
42
+
43
+ interface ProjectContextType {
44
+ org: {
45
+ id: string;
46
+ name: string;
47
+ slug: string;
48
+ logo: string | null;
49
+ };
50
+
51
+ project: {
52
+ name?: string;
53
+ slug: string;
54
+ };
55
+
56
+ locator: ProjectLocator;
57
+ }
58
+
59
+ const ProjectContext = createContext<ProjectContextType | undefined>(undefined);
60
+
61
+ export const useProjectContext = () => {
62
+ const context = useContext(ProjectContext);
63
+ if (!context) {
64
+ throw new Error(
65
+ "useProjectContext must be used within a ProjectContextProvider",
66
+ );
67
+ }
68
+
69
+ return context;
70
+ };
71
+
72
+ export type ProjectContextProviderProps = {
73
+ org: { id: string; slug: string; name: string; logo: string | null };
74
+ project: { name?: string; slug: string };
75
+ };
76
+
77
+ export const ProjectContextProvider = ({
78
+ children,
79
+ org,
80
+ project,
81
+ }: PropsWithChildren<ProjectContextProviderProps>) => {
82
+ const locator = Locator.from({ org: org.slug, project: project.slug });
83
+
84
+ const value = { org, project, locator };
85
+
86
+ return (
87
+ <ProjectContext.Provider value={value}>{children}</ProjectContext.Provider>
88
+ );
89
+ };
@@ -0,0 +1,73 @@
1
+ // Collection hooks
2
+ export {
3
+ useCollectionItem,
4
+ useCollectionList,
5
+ useCollectionActions,
6
+ type CollectionEntity,
7
+ type CollectionFilter,
8
+ type UseCollectionListOptions,
9
+ } from "./use-collections";
10
+
11
+ // Connection hooks
12
+ export {
13
+ useConnections,
14
+ useConnection,
15
+ useConnectionActions,
16
+ type ConnectionFilter,
17
+ type UseConnectionsOptions,
18
+ } from "./use-connection";
19
+
20
+ // MCP client hook and factory
21
+ export {
22
+ createMCPClient,
23
+ useMCPClient,
24
+ type CreateMcpClientOptions,
25
+ type UseMcpClientOptions,
26
+ } from "./use-mcp-client";
27
+
28
+ // MCP tools hooks
29
+ export {
30
+ useMCPToolsList,
31
+ useMCPToolsListQuery,
32
+ useMCPToolCall,
33
+ useMCPToolCallQuery,
34
+ useMCPToolCallMutation,
35
+ type UseMcpToolsListOptions,
36
+ type UseMcpToolsListQueryOptions,
37
+ type UseMcpToolCallOptions,
38
+ type UseMcpToolCallQueryOptions,
39
+ type UseMcpToolCallMutationOptions,
40
+ } from "./use-mcp-tools";
41
+
42
+ // MCP resources hooks and helpers
43
+ export {
44
+ listResources,
45
+ readResource,
46
+ useMCPResourcesList,
47
+ useMCPResourcesListQuery,
48
+ useMCPReadResource,
49
+ type UseMcpResourcesListOptions,
50
+ type UseMcpResourcesListQueryOptions,
51
+ type UseMcpReadResourceOptions,
52
+ } from "./use-mcp-resources";
53
+
54
+ // MCP prompts hooks and helpers
55
+ export {
56
+ listPrompts,
57
+ getPrompt,
58
+ useMCPPromptsList,
59
+ useMCPPromptsListQuery,
60
+ useMCPGetPrompt,
61
+ type UseMcpPromptsListOptions,
62
+ type UseMcpPromptsListQueryOptions,
63
+ type UseMcpGetPromptOptions,
64
+ } from "./use-mcp-prompts";
65
+
66
+ // Virtual MCP hooks
67
+ export {
68
+ useVirtualMCPs,
69
+ useVirtualMCP,
70
+ useVirtualMCPActions,
71
+ type VirtualMCPFilter,
72
+ type UseVirtualMCPsOptions,
73
+ } from "./use-virtual-mcp";