192 lines · 6.9 KB · TypeScript
RawDownload
/** * Example: Promise-based executor SDK with MCP, OpenAPI, and GraphQL * — no Effect knowledge or database setup needed. Uses the SDK's * ephemeral in-memory FumaDB backend by default. * * v2 model: an *integration* is the API surface (added via the plugin's * extension), and a *connection* is the credential for that integration. Tools * are produced per connection and addressed as * `tools.<integration>.<owner>.<connection>.<tool>`. A connection's value lives * in a writable CredentialProvider — here a tiny in-memory store registered via * `providers`. There is no separate secret store: a connection *is* the secret. */import {  createExecutor,  ProviderItemId,  ProviderKey,  type CredentialProvider,} from "@executor-js/sdk/promise";import { Effect } from "effect";import { mcpPlugin } from "@executor-js/plugin-mcp/promise";import { openApiPlugin, variable } from "@executor-js/plugin-openapi/promise";import { graphqlPlugin } from "@executor-js/plugin-graphql/promise"; // ---------------------------------------------------------------------------// 1. Create the executor with all plugins//// A connection stores its value in a writable credential provider. This tiny// in-memory store is enough for a script; production hosts swap in a durable// provider (keychain, 1Password, an encrypted DB store). Providers are// Effect-native, so `get`/`set` return `Effect`s.// --------------------------------------------------------------------------- const plugins = [mcpPlugin(), openApiPlugin(), graphqlPlugin()] as const; const memory = new Map<string, string>();const memoryProvider: CredentialProvider = {  key: ProviderKey.make("memory"),  writable: true,  get: (id: ProviderItemId) => Effect.sync(() => memory.get(String(id)) ?? null),  set: (id: ProviderItemId, value: string) =>    Effect.sync(() => {      memory.set(String(id), value);    }),}; const executor = await createExecutor({  plugins,  providers: [memoryProvider],  onElicitation: "accept-all",}); // ---------------------------------------------------------------------------// 2. MCP — register a remote server as an integration, then connect to it.//// `addServer` registers the catalog entry; `connections.create` produces the// per-connection tools. Context7 needs no credential, so the connection's value// is an empty string applied through the "none" template.// --------------------------------------------------------------------------- const context7 = await executor.mcp.addServer({  transport: "remote",  name: "Context7",  endpoint: "https://mcp.context7.com/mcp",  slug: "context7",}); await executor.connections.create({  owner: "org",  name: "default",  integration: context7.slug,  template: "none",  value: "",}); // Stdio server (disabled by default — pass `dangerouslyAllowStdioMCP: true` to// mcpPlugin() to enable, only for trusted local contexts):// await executor.mcp.addServer({//   transport: "stdio",//   name: "My Server",//   command: "npx",//   args: ["-y", "@my/mcp-server"],//   slug: "my-server",// }); // ---------------------------------------------------------------------------// 3. OpenAPI — load a spec by URL as an integration, then connect.//// Petstore is public, so the connection carries a throwaway value. To require a// real key, declare an `authenticationTemplate` (see Stripe below) and create a// connection whose `value` is the token.// --------------------------------------------------------------------------- await executor.openapi.addSpec({  spec: {    kind: "url",    url: "https://petstore3.swagger.io/api/v3/openapi.json",  },  slug: "petstore",  description: "Petstore",  baseUrl: "https://petstore3.swagger.io/api/v3",}); await executor.connections.create({  owner: "org",  name: "default",  integration: "petstore",  template: "none",  value: "",}); // Auth-backed integration: declare where the connection's value renders (here an// `Authorization: Bearer <token>` header), then create a connection with the// real token. The value is written to the `memory` provider and applied to the// template lazily, per request — never pre-baked into the spec.// await executor.openapi.addSpec({//   spec: { kind: "url", url: "https://raw.githubusercontent.com/.../stripe.json" },//   slug: "stripe",//   authenticationTemplate: [//     {//       slug: "bearer",//       type: "apiKey",//       headers: { Authorization: ["Bearer ", variable("token")] },//     },//   ],// });// await executor.connections.create({//   owner: "org",//   name: "default",//   integration: "stripe",//   template: "bearer",//   value: "sk_live_...",// });void variable; // referenced by the commented authoring example above // ---------------------------------------------------------------------------// 4. GraphQL — introspect an endpoint as an integration, then connect.// --------------------------------------------------------------------------- await executor.graphql.addIntegration({  endpoint: "https://graphql.anilist.co",  name: "AniList",  slug: "anilist",}); await executor.connections.create({  owner: "org",  name: "default",  integration: "anilist",  template: "none",  value: "",}); // ---------------------------------------------------------------------------// 5. Unified tool catalog — all plugins, one list, addressed per connection.// --------------------------------------------------------------------------- const tools = await executor.tools.list();console.log(`\n${tools.length} tools across all plugins:`);for (const t of tools) {  console.log(`  [${t.pluginId}] ${t.address} — ${t.description}`);} const firstPetstoreTool = tools.find((t) => t.integration === "petstore");if (firstPetstoreTool) {  const schema = await executor.tools.schema(firstPetstoreTool.address);  console.log(`\n${firstPetstoreTool.name} input: ${schema?.inputTypeScript ?? "<none>"}`);} // ---------------------------------------------------------------------------// 6. Execute tools — same interface regardless of plugin. The executor resolves// the connection's credential and hands it to the owning plugin.// --------------------------------------------------------------------------- const anilistTool = tools.find((t) => t.integration === "anilist");if (anilistTool) {  const result = await executor.execute(anilistTool.address, {});  console.log("\nResult:", result);} // ---------------------------------------------------------------------------// 7. Connections are the credentials — list and inspect them across all plugins.// (v2 has no separate `executor.secrets`; a connection IS the saved credential,// and its value lives in a registered provider.)// --------------------------------------------------------------------------- const connections = await executor.connections.list();console.log(`\n${connections.length} connections:`);for (const c of connections) {  console.log(`  ${c.address} (provider: ${c.provider})`);} await executor.close();