diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts index 74b4284..4b1375b 100644 --- a/docs/.vitepress/config.ts +++ b/docs/.vitepress/config.ts @@ -53,6 +53,7 @@ export default defineConfig({ { text: 'ai-autopilot', link: '/packages/ai-autopilot' }, { text: 'ai-mcp', link: '/packages/ai-mcp' }, { text: 'mcp', link: '/packages/mcp' }, + { text: 'connectors', link: '/packages/connectors' }, ], }, { @@ -91,6 +92,15 @@ export default defineConfig({ { text: 'mcp', link: '/packages/mcp' }, ], }, + { + text: 'Connectors', + items: [ + { text: 'connectors — the contract', link: '/packages/connectors' }, + { text: 'The connector registry', link: '/packages/connectors-registry' }, + { text: 'connector-github', link: '/packages/connector-github' }, + { text: 'connector-google-drive', link: '/packages/connector-google-drive' }, + ], + }, ], }, diff --git a/docs/packages/connector-github.md b/docs/packages/connector-github.md new file mode 100644 index 0000000..7d69b9a --- /dev/null +++ b/docs/packages/connector-github.md @@ -0,0 +1,51 @@ +# @gemstack/connector-github + +A GitHub connector for GemStack AI orchestration. Read and act on issues, pull requests, and repository files over the GitHub REST API. Built with [`@gemstack/connectors`](/packages/connectors); the result is a standard [`@gemstack/mcp`](/packages/mcp) server. + +## Installation + +```bash +npm i @gemstack/connector-github @gemstack/connectors @gemstack/mcp +``` + +## Use + +```ts +import { mountConnectors } from '@gemstack/connectors' +import { Mcp } from '@gemstack/mcp' +import github from '@gemstack/connector-github' + +const Server = mountConnectors([github], { + // A token with `repo` scope. A PAT or an OAuth bearer both work. + credentials: () => ({ token: process.env.GITHUB_TOKEN }), +}) + +Mcp.web('/mcp/github', Server) +``` + +Tools are exposed namespaced by connector id, e.g. `github_list-issues`. + +## Auth + +The connector declares `auth: { type: 'pat', env: 'GITHUB_TOKEN' }`. It only consumes a bearer token from `ctx.auth.token` — it does no OAuth handshake itself. To protect a web endpoint with OAuth 2.1, wrap it with `@gemstack/mcp`'s `oauth2McpMiddleware` + `registerOAuth2Metadata` and feed the verified token through the mount `credentials` option. + +## Tools + +| Tool | Kind | Description | +|---|---|---| +| `get-repo` | read | Repository metadata | +| `list-issues` | read | Issues (excludes PRs) | +| `get-issue` | read | One issue by number | +| `list-pull-requests` | read | Pull requests | +| `get-pull-request` | read | One PR by number | +| `get-file` | read | File contents (base64-decoded) | +| `search-issues` | read | Search issues/PRs (GitHub search syntax) | +| `comment-on-issue` | write | Comment on an issue or PR | +| `create-issue` | write | Open a new issue | + +Read tools are annotated `readOnly` so an agent can auto-approve them; write tools are not. Responses are slimmed to the fields an agent needs (no raw API envelopes) to keep token usage down. + +## See also + +- [`@gemstack/connectors`](/packages/connectors) — the contract this is built on, and how to write your own. +- [The connector registry](/packages/connectors-registry) — the other published connectors. diff --git a/docs/packages/connector-google-drive.md b/docs/packages/connector-google-drive.md new file mode 100644 index 0000000..bb13a81 --- /dev/null +++ b/docs/packages/connector-google-drive.md @@ -0,0 +1,51 @@ +# @gemstack/connector-google-drive + +A Google Drive connector for GemStack AI orchestration. Browse, read, and share Drive files over the Drive REST API (v3). Built with [`@gemstack/connectors`](/packages/connectors); the result is a standard [`@gemstack/mcp`](/packages/mcp) server. + +## Installation + +```bash +npm i @gemstack/connector-google-drive @gemstack/connectors @gemstack/mcp +``` + +## Use + +```ts +import { mountConnectors } from '@gemstack/connectors' +import { Mcp } from '@gemstack/mcp' +import drive from '@gemstack/connector-google-drive' + +const Server = mountConnectors([drive], { + // A Google OAuth 2.0 access token with a Drive scope. + credentials: () => ({ token: process.env.GOOGLE_ACCESS_TOKEN }), +}) + +Mcp.web('/mcp/drive', Server) +``` + +Tools are exposed namespaced by connector id, e.g. `google-drive_list-files`. + +## Auth + +Drive has no static API key — it is OAuth 2.0 only. The connector declares `auth: { type: 'oauth', scopes: ['https://www.googleapis.com/auth/drive'] }` and consumes a bearer token from `ctx.auth.token`; it does no OAuth handshake itself. Use `drive.readonly` if you only need the read tools. To protect a web endpoint and obtain the token, wrap it with `@gemstack/mcp`'s `oauth2McpMiddleware` + `registerOAuth2Metadata` and feed the verified token through the mount `credentials` option. + +## Tools + +| Tool | Kind | Description | +|---|---|---| +| `get-about` | read | Authenticated user + storage usage | +| `list-files` | read | List files (folder / raw query scoped) | +| `search-files` | read | Search by name and full-text content | +| `get-file` | read | Metadata for one file or folder | +| `get-file-content` | read | File as text (Docs exported, others downloaded) | +| `list-permissions` | read | Who has access to a file | +| `create-folder` | write | Create a folder | +| `share-file` | write | Grant access (create a permission) | +| `trash-file` | write (destructive) | Move a file to the trash (reversible) | + +`get-file-content` exports Google Docs/Sheets/Slides to text/CSV and downloads everything else via `alt=media`. Read tools are annotated `readOnly` so an agent can auto-approve them; `trash-file` is marked `destructive`. Responses are slimmed to the fields an agent needs (no raw API envelopes) to keep token usage down. + +## See also + +- [`@gemstack/connectors`](/packages/connectors) — the contract this is built on, and how to write your own. +- [The connector registry](/packages/connectors-registry) — the other published connectors. diff --git a/docs/packages/connectors-registry.md b/docs/packages/connectors-registry.md new file mode 100644 index 0000000..59c74b4 --- /dev/null +++ b/docs/packages/connectors-registry.md @@ -0,0 +1,32 @@ +# The connector registry + +Connectors are the open, copyable layer of GemStack orchestration. Anyone can ship one: build it with [`@gemstack/connectors`](/packages/connectors), publish it to npm under the `connector-*` convention, and an orchestrator can mount it next to the first-party connectors with no special blessing. + +## First-party connectors + +| Connector | Package | Auth | What it does | +|---|---|---|---| +| [GitHub](/packages/connector-github) | `@gemstack/connector-github` | PAT / OAuth bearer | Read and act on issues, pull requests, and repository files. | +| [Google Drive](/packages/connector-google-drive) | `@gemstack/connector-google-drive` | Google OAuth 2.0 | Browse, read, and share Drive files (Docs/Sheets/Slides exported to text). | + +Both are thin connectors over a REST client on the [`@gemstack/connectors`](/packages/connectors) contract — read them as canonical examples when writing your own. + +## Naming convention + +Publish a connector package as **`connector-`** (first-party: `@gemstack/connector-github`; third-party: `@your-scope/connector-acme` or `gemstack-connector-acme` unscoped). The connector's runtime `id` — the value that namespaces its tools — should match the service (`github`, `google-drive`, `acme`). + +The convention is what makes connectors discoverable: a search for `connector-` on npm, or the GitHub topic [`gemstack-connector`](https://github.com/topics/gemstack-connector), surfaces the ecosystem the same way [Vike's extensions listing](https://vike.dev/extensions) does for Vike. + +## Publish your own + +1. Build it with [`@gemstack/connectors`](/packages/connectors) — start from `examples/connectors-quickstart`. +2. Name the package `connector-` and add the `gemstack-connector` keyword + GitHub topic. +3. Declare its `auth` honestly (`none` / `pat` / `oauth`) so an orchestrator knows what credential to resolve. +4. Publish to npm. That's it — there's no registry to register with; the naming convention *is* the registry. + +To get a third-party connector linked from this page, open a PR or an issue on [gemstack-land/gemstack](https://github.com/gemstack-land/gemstack/issues). + +## See also + +- [`@gemstack/connectors`](/packages/connectors) — the contract and the "writing a connector" guide. +- [`mcp`](/packages/mcp) — the server a mounted connector becomes. diff --git a/docs/packages/connectors.md b/docs/packages/connectors.md new file mode 100644 index 0000000..f30999a --- /dev/null +++ b/docs/packages/connectors.md @@ -0,0 +1,140 @@ +# @gemstack/connectors + +The **connector contract** for GemStack AI orchestration. Define a tool connector to an external service (GitHub, Google Drive, ...) once with `defineConnector`, then compose any number of connectors into a single [`@gemstack/mcp`](/packages/mcp) server with `mountConnectors`. + +A connector only **declares** what it needs (its auth requirement) and **what it does** (its tools). It never reaches for env vars, OAuth, or a transport itself. The orchestrator that mounts it supplies credentials and chooses how to serve it. That split is what lets first-party and third-party connectors compose interchangeably. + +> Already looking for a specific service? See the [connector registry](/packages/connectors-registry) for the published connectors (GitHub, Google Drive) and the `connector-*` naming convention. + +## Installation + +```bash +npm i @gemstack/connectors @gemstack/mcp zod +``` + +The server it produces plugs into the same surface as any other `@gemstack/mcp` server (`Mcp.web` / `Mcp.local`, `McpTestClient`, the neutral HTTP handler). + +## Writing a connector + +A connector is one object passed to `defineConnector`: an `id` (used to namespace its tools), an `auth` requirement, and a list of `tools`. Here is the reference connector shipped in `examples/connectors-quickstart` — a read-only `library` over an in-memory list. Copy it to start a real one: swap the in-memory data for calls to your API, and change `auth` from `none` to `pat` / `oauth`. + +```ts +import { defineConnector } from '@gemstack/connectors' +import { z } from 'zod' + +const BOOKS = [ + { id: 'b1', title: 'The Pragmatic Programmer', author: 'Hunt & Thomas' }, + { id: 'b2', title: 'Refactoring', author: 'Fowler' }, +] + +export default defineConnector({ + id: 'library', // lowercase letters, digits, '-'; namespaces this connector's tools + name: 'Reference Library', + instructions: 'A read-only demo connector over a small in-memory book list.', + auth: { type: 'none' }, // 'none' | 'pat' | 'oauth' + tools: [ + { + name: 'list-books', + description: 'List every book in the library.', + schema: z.object({}), + annotations: { readOnly: true }, + handle: () => BOOKS, + }, + { + name: 'get-book', + description: 'Fetch one book by id.', + schema: z.object({ id: z.string() }), + annotations: { readOnly: true }, + handle: (input: { id: string }, ctx) => { + // ctx.auth carries the credential the orchestrator resolved for 'library'. + return BOOKS.find((b) => b.id === input.id) ?? { error: `no book ${input.id}` } + }, + }, + ], +}) +``` + +### A tool + +Each entry in `tools` is one tool the connector exposes: + +| Field | Required | Purpose | +|---|---|---| +| `name` | yes | Unique within the connector. Kept verbatim, namespaced at mount (`library` + `get-book` -> `library_get-book`). | +| `description` | recommended | One line shown to the agent. | +| `schema` | yes | A Zod object (v3 or v4). The runtime source of truth for input validation. | +| `annotations` | no | Behavioural hints: `readOnly`, `destructive`, `idempotent`, `openWorld`. Let agents reason before calling (e.g. auto-approve `readOnly`). | +| `handle` | yes | `(input, ctx) => result`. Receives validated `input` and the connector [context](#the-handler-context). | + +A `handle` may return a `string` (wrapped as text), any JSON-serializable value (wrapped as pretty JSON), or a full `McpToolResult` (use `McpResponse` from `@gemstack/mcp` for errors / images). + +### The handler context + +The second `handle` argument is the `ConnectorContext`: + +- `ctx.connectorId` — the id of the connector this tool belongs to. +- `ctx.auth` — the `Credential` the orchestrator resolved for this connector (`{}` when none was provided). `ctx.auth.token` is the common case (a PAT or OAuth bearer). + +A connector never reads `process.env` or runs an OAuth handshake itself — it reads `ctx.auth.token` and lets the orchestrator decide where that came from. See the [GitHub](/packages/connector-github) and [Google Drive](/packages/connector-google-drive) connectors for the same pattern over a real REST client. + +## Auth requirements + +A connector declares one of: + +| `auth.type` | Means | Credential the orchestrator provides | +|---|---|---| +| `none` | Public / local data | `{}` | +| `pat` | Personal access token / API key (`env` names a default var) | `{ token }` | +| `oauth` | OAuth 2.1 bearer (`scopes`, `authorizationServers`) | `{ token }` | + +For `oauth`, protect the mounted endpoint with `@gemstack/mcp`'s `oauth2McpMiddleware` + `registerOAuth2Metadata` and feed the verified token through `credentials` (below). + +## Mount connectors into a server + +`mountConnectors` composes any number of connectors into one standard `@gemstack/mcp` server class. + +```ts +import { mountConnectors } from '@gemstack/connectors' +import { Mcp } from '@gemstack/mcp' +import library from './library-connector.js' +import github from '@gemstack/connector-github' + +const Server = mountConnectors([library, github], { + name: 'my-orchestrator', + // Resolve each connector's credential at call time. This is the seam that + // satisfies a connector's declared `auth`. + credentials: (id) => ({ token: process.env[`${id.toUpperCase()}_TOKEN`] }), +}) + +Mcp.web('/mcp/connectors', Server) // a standard @gemstack/mcp server +``` + +Tools are namespaced by connector id, so `library.get-book` is exposed as `library_get-book` and never collides with another connector's `get-book`. Pass `namespace: 'none'` to keep names verbatim (you then own collision-avoidance). + +## Testing + +A mounted connector is a normal `@gemstack/mcp` server, so drive it with `McpTestClient` — no transport, no network: + +```ts +import { McpTestClient } from '@gemstack/mcp/testing' +import { mountConnectors } from '@gemstack/connectors' +import library from './library-connector.js' + +const client = new McpTestClient(mountConnectors([library])) + +await client.listTools() // [{ name: 'library_list-books', ... }, ...] +await client.callTool('library_get-book', { id: 'b1' }) +``` + +To test a connector that calls a real API, stub global `fetch` and assert on the requests it makes — that is exactly how the [GitHub](https://github.com/gemstack-land/gemstack/blob/main/packages/connector-github/src/index.test.ts) and [Google Drive](https://github.com/gemstack-land/gemstack/blob/main/packages/connector-google-drive/src/index.test.ts) connectors are tested. + +## API + +- `defineConnector(def): Connector` — validate the definition + fill defaults. +- `mountConnectors(connectors, options?): ConnectorServerClass` — compose into one `@gemstack/mcp` server class. Options: `name`, `version`, `instructions`, `namespace` (`'id'` default | `'none'`), and `credentials(id) => Credential`. + +## See also + +- [The connector registry](/packages/connectors-registry) — published connectors + the `connector-*` convention for shipping your own. +- [`mcp`](/packages/mcp) — the server framework a mounted connector becomes. +- [`ai-mcp`](/packages/ai-mcp) — feed a mounted connector's tools into an agent. diff --git a/docs/packages/index.md b/docs/packages/index.md index e7fefc0..d911f19 100644 --- a/docs/packages/index.md +++ b/docs/packages/index.md @@ -9,6 +9,7 @@ All packages publish under the **`@gemstack/`** scope (e.g. `npm install @gemsta | [`ai-autopilot`](/packages/ai-autopilot) | Orchestration: a Supervisor that plans, dispatches subagents (bounded concurrency + budget guardrails), and synthesizes the result. | | [`ai-mcp`](/packages/ai-mcp) | The agent/MCP bridge: consume a remote MCP server's tools as agent tools, and expose an agent as an MCP server. | | [`mcp`](/packages/mcp) | A standalone framework for *authoring* MCP servers: tools, resources, prompts, decorators, OAuth 2.1, a framework-neutral HTTP handler, and a test client. Agent-agnostic. | +| [`connectors`](/packages/connectors) | The connector contract: `defineConnector` declares a tool connector to an external service; `mountConnectors` composes any number into one `@gemstack/mcp` server. First-party connectors: [GitHub](/packages/connector-github), [Google Drive](/packages/connector-google-drive). | ## How they fit together @@ -19,8 +20,13 @@ ai-autopilot orchestration / autonomy (the "director") -> ai-sdk (+ skills) ai-mcp agent <-> MCP bridge (the "adapter") -> ai-sdk ----------------------------------------------------------------------------------- mcp standalone MCP server framework agent-agnostic, not ai-* +connectors tool connectors to external services -> mcp (composes into one) ``` +### Connectors + +`connectors` builds *on* `mcp`: a connector declares an external service's auth requirement and tools, and `mountConnectors` composes any number of them into a single `mcp` server. It is the open, copyable layer — third parties ship their own `connector-*` packages that mount alongside the first-party [GitHub](/packages/connector-github) and [Google Drive](/packages/connector-google-drive) connectors. See [the connector registry](/packages/connectors-registry). + ### Two MCP packages, two jobs `ai-mcp` and `mcp` both touch the Model Context Protocol, but from opposite ends: