connectors 0.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.
Files changed (112) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +35 -0
  3. data/CONNECTORS_FRAMEWORK.md +799 -0
  4. data/CONTRIBUTING.md +60 -0
  5. data/MCP_CLIENT.md +168 -0
  6. data/MIT-LICENSE +20 -0
  7. data/README.md +146 -0
  8. data/app/connectors/clickup/connector.rb +13 -0
  9. data/app/connectors/gmail/api.rb +114 -0
  10. data/app/connectors/gmail/connector.rb +921 -0
  11. data/app/connectors/gmail/mime_builder.rb +261 -0
  12. data/app/connectors/gmail/mime_parser.rb +106 -0
  13. data/app/connectors/gmail/polling.rb +154 -0
  14. data/app/connectors/remote_mcp/connector.rb +18 -0
  15. data/app/connectors/resend/connector.rb +218 -0
  16. data/app/controllers/concerns/connectors/grant_access.rb +43 -0
  17. data/app/controllers/connectors/actions_controller.rb +66 -0
  18. data/app/controllers/connectors/application_controller.rb +5 -0
  19. data/app/controllers/connectors/credentials_controller.rb +245 -0
  20. data/app/controllers/connectors/grants_controller.rb +123 -0
  21. data/app/controllers/connectors/mcp_controller.rb +88 -0
  22. data/app/controllers/connectors/oauth_controller.rb +134 -0
  23. data/app/controllers/connectors/types_controller.rb +144 -0
  24. data/app/controllers/connectors/webhooks_controller.rb +105 -0
  25. data/app/jobs/connectors/application_job.rb +4 -0
  26. data/app/jobs/connectors/deliver_webhook_job.rb +27 -0
  27. data/app/jobs/connectors/poll_job.rb +49 -0
  28. data/app/models/connectors/application_record.rb +5 -0
  29. data/app/models/connectors/credential_share.rb +31 -0
  30. data/app/models/connectors/grant.rb +103 -0
  31. data/app/models/connectors/mcp_authorization.rb +6 -0
  32. data/app/models/connectors/mcp_interaction.rb +6 -0
  33. data/app/models/connectors/poll_state.rb +17 -0
  34. data/app/models/connectors/webhook_event.rb +19 -0
  35. data/config/routes.rb +68 -0
  36. data/db/migrate/20260518210324_create_connectors_grants.rb +49 -0
  37. data/db/migrate/20260518214609_create_connectors_webhook_events.rb +35 -0
  38. data/db/migrate/20260521140000_create_connectors_credential_shares.rb +26 -0
  39. data/db/migrate/20260922120000_create_connectors_poll_states.rb +12 -0
  40. data/db/migrate/20260922130000_create_connectors_mcp_transactions.rb +18 -0
  41. data/docs/adding-connectors.md +92 -0
  42. data/docs/architecture.md +71 -0
  43. data/docs/releasing.md +60 -0
  44. data/lib/connectors/action.rb +90 -0
  45. data/lib/connectors/action_builder.rb +125 -0
  46. data/lib/connectors/action_runner.rb +99 -0
  47. data/lib/connectors/auth/scheme/api_key.rb +51 -0
  48. data/lib/connectors/auth/scheme/oauth2.rb +23 -0
  49. data/lib/connectors/auth/scheme.rb +35 -0
  50. data/lib/connectors/auth.rb +4 -0
  51. data/lib/connectors/auth_injection.rb +65 -0
  52. data/lib/connectors/client_builder.rb +87 -0
  53. data/lib/connectors/configuration.rb +138 -0
  54. data/lib/connectors/connector.rb +565 -0
  55. data/lib/connectors/credential_schema.rb +219 -0
  56. data/lib/connectors/credential_tester.rb +85 -0
  57. data/lib/connectors/credential_type_registry.rb +162 -0
  58. data/lib/connectors/credential_types/http_auth.rb +171 -0
  59. data/lib/connectors/engine.rb +123 -0
  60. data/lib/connectors/errors.rb +88 -0
  61. data/lib/connectors/grant_policy.rb +13 -0
  62. data/lib/connectors/mcp/access.rb +50 -0
  63. data/lib/connectors/mcp/authorization.rb +177 -0
  64. data/lib/connectors/mcp/authorization_context.rb +25 -0
  65. data/lib/connectors/mcp/authorization_discovery.rb +42 -0
  66. data/lib/connectors/mcp/cancellation.rb +36 -0
  67. data/lib/connectors/mcp/client.rb +137 -0
  68. data/lib/connectors/mcp/connection_config.rb +48 -0
  69. data/lib/connectors/mcp/http.rb +108 -0
  70. data/lib/connectors/mcp/interaction.rb +82 -0
  71. data/lib/connectors/mcp/pending_transaction.rb +26 -0
  72. data/lib/connectors/mcp/protocol/2026-07-28.json +3963 -0
  73. data/lib/connectors/mcp/protocol/LICENSE +216 -0
  74. data/lib/connectors/mcp/protocol/README.md +8 -0
  75. data/lib/connectors/mcp/protocol_schema.rb +30 -0
  76. data/lib/connectors/mcp/schema.rb +45 -0
  77. data/lib/connectors/mcp/settings.rb +27 -0
  78. data/lib/connectors/mcp/token_endpoint.rb +48 -0
  79. data/lib/connectors/mcp/transport.rb +105 -0
  80. data/lib/connectors/mcp.rb +69 -0
  81. data/lib/connectors/middleware/authenticate_generic.rb +79 -0
  82. data/lib/connectors/middleware/auto_refresh.rb +71 -0
  83. data/lib/connectors/middleware/error_normalization.rb +45 -0
  84. data/lib/connectors/middleware/grant_status.rb +19 -0
  85. data/lib/connectors/middleware/pre_authentication.rb +54 -0
  86. data/lib/connectors/middleware/rate_limit.rb +40 -0
  87. data/lib/connectors/oauth/authorize_url.rb +78 -0
  88. data/lib/connectors/oauth/client_authentication.rb +24 -0
  89. data/lib/connectors/oauth/client_credentials.rb +43 -0
  90. data/lib/connectors/oauth/grant_writer.rb +73 -0
  91. data/lib/connectors/oauth/pkce.rb +32 -0
  92. data/lib/connectors/oauth/revoke.rb +72 -0
  93. data/lib/connectors/oauth/state.rb +41 -0
  94. data/lib/connectors/oauth/token_exchange.rb +69 -0
  95. data/lib/connectors/oauth/token_response.rb +40 -0
  96. data/lib/connectors/oauth.rb +4 -0
  97. data/lib/connectors/oauth1.rb +151 -0
  98. data/lib/connectors/permission_check.rb +33 -0
  99. data/lib/connectors/poll_runner.rb +50 -0
  100. data/lib/connectors/pre_authentication_helpers.rb +76 -0
  101. data/lib/connectors/registry.rb +38 -0
  102. data/lib/connectors/version.rb +3 -0
  103. data/lib/connectors/webhook_context.rb +62 -0
  104. data/lib/connectors/webhook_lifecycle.rb +69 -0
  105. data/lib/connectors/webhook_methods.rb +48 -0
  106. data/lib/connectors/webhooks/verifier.rb +31 -0
  107. data/lib/connectors/webhooks.rb +5 -0
  108. data/lib/connectors.rb +50 -0
  109. data/lib/tasks/connectors_mcp.rake +9 -0
  110. data/lib/tasks/connectors_tasks.rake +4 -0
  111. data/openapi.yaml +1099 -0
  112. metadata +263 -0
data/CONTRIBUTING.md ADDED
@@ -0,0 +1,60 @@
1
+ # Development and contributing
2
+
3
+ Read the [README](README.md) for host setup, the [architecture](docs/architecture.md) for execution boundaries, and the [connector guide](docs/adding-connectors.md) for provider additions.
4
+
5
+ ## Local setup
6
+
7
+ Use Ruby 3.4.8 (`.ruby-version`), PostgreSQL 16 and Python 3.9+ for the independent MCP fixture. Bundler installs the development/test dependencies from `Gemfile`; consumers receive only `connectors.gemspec` runtime dependencies. `Gemfile.lock` records the development baseline.
8
+
9
+ From the repository root:
10
+
11
+ ```sh
12
+ bundle install
13
+ export RAILS_ENV=test
14
+ export DATABASE_URL=postgresql://localhost/connectors_test
15
+ bundle exec rake app:db:prepare
16
+ bundle exec rspec --order random
17
+ bundle exec rubocop
18
+ ```
19
+
20
+ Use a disposable database: the suite includes committed-record concurrency tests and cleanup. Do not point it at a host's development or production data. PostgreSQL role permissions must allow database creation, or create the test database beforehand. `PYTHON` can select the Python executable; otherwise tests use `python3`. Local socket/loopback access is required by MCP interoperability and streaming tests.
21
+
22
+ Use a TCP database connection, as CI does, when validating concurrency changes. MCP tests use `stub_mcp_dns` to replace only provider fixture lookups; never stub all DNS resolution, because the PostgreSQL driver uses it too.
23
+
24
+ The dummy Rails application lives in `test/dummy`. It provides UUID owners, synthetic encryption keys, test routes and fixture connectors. Its keys and authentication setup are not installation defaults for host applications.
25
+
26
+ ## Test workflow
27
+
28
+ For behavior changes, first write a failing RSpec example that demonstrates the requirement; run it, implement the smallest change, then rerun it. Use request tests for HTTP access/response contracts and service tests for Ruby entry points. Stub external providers with WebMock; never require personal credentials or real writes in the automated suite. Use real PostgreSQL connections for locking/concurrency claims.
29
+
30
+ Examples:
31
+
32
+ ```sh
33
+ bundle exec rspec spec/requests/credentials_crud_spec.rb
34
+ bundle exec rspec spec/connectors/mcp/client_spec.rb
35
+ bundle exec rspec --order random
36
+ bundle exec rubocop
37
+ ```
38
+
39
+ Report the command and seed when reporting a failure. A test count is not a coverage percentage; there is currently no enforced line/branch coverage threshold.
40
+
41
+ ## Package verification
42
+
43
+ ```sh
44
+ bundle exec ruby script/verify_package.rb
45
+ ```
46
+
47
+ This strict-builds the gem, checks required files and local documentation links, installs it into a temporary gem directory without downloading dependencies, then boots a separate Rails host against the installed artifact. It checks the shipped registry, migration paths and vendored MCP schema. Dependencies must already be installed with Bundler. Temporary artifacts are removed automatically. The same check runs through `spec/packaging_spec.rb` in the full suite, so CI exercises it too.
48
+
49
+ The test suite validates provider behavior with fixtures and local interoperability servers. It does not certify live provider availability, account permissions, every supported Ruby version, or production load.
50
+
51
+ ## Change conventions
52
+
53
+ - Keep provider-specific code under `app/connectors/<provider>/`; reusable mechanisms belong under `lib/connectors/`.
54
+ - Add host requirements, examples and API changes to the relevant guide and `openapi.yaml`. Record compatibility changes under `Unreleased` in `CHANGELOG.md`.
55
+ - Avoid provider secrets in fixtures, logs, failures and screenshots. Use synthetic credentials and sanitized captured public metadata.
56
+ - Do not edit already released migrations to upgrade existing hosts. Add migrations and upgrade instructions.
57
+ - For protocol changes, retain upstream attribution and add contract/interoperability tests. See the [vendored schema notes](lib/connectors/mcp/protocol/README.md).
58
+ - Keep HTTP retries explicit, particularly for actions that can produce side effects.
59
+
60
+ Before handing off a change, include the problem, changed behavior, tests run and any unresolved limitation. Release preparation is documented separately in [releasing](docs/releasing.md).
data/MCP_CLIENT.md ADDED
@@ -0,0 +1,168 @@
1
+ # External MCP connections
2
+
3
+ The engine consumes remote MCP **2026-07-28** servers through Streamable HTTP. It supports public servers, bearer tokens, custom authentication headers, and user OAuth with discovery, PKCE, registration, refresh and scope upgrades. Tools remain scoped to their grant; they are not added to the global connector registry.
4
+
5
+ The implementation uses public helpers from `mcp` **1.6.0**, `json_schemer` **2.5**, and `event_stream_parser` **1.0**. Its durable OAuth coordinator and HTTP transport are owned by this engine. The SDK's synchronous OAuth flow and high-level tool projection did not meet the reviewed requirements. No SDK private methods are patched or invoked. Server result types are validated against the vendored, versioned official schema; its source and license are in [protocol/README.md](lib/connectors/mcp/protocol/README.md).
6
+
7
+ ## Installation and host setup
8
+
9
+ Run `bundle install` and the host's migrations. The new migration creates encrypted, expiring authorization and interaction records, with PostgreSQL foreign keys and one-time claims. Existing connector tables and credentials are preserved.
10
+
11
+ Follow the [host installation guide](README.md#installation), including encryption and trusted authentication middleware. Configure the engine's existing owner/principal resolvers. These remain the authentication boundary; callers must never supply their own trusted principal list through request parameters.
12
+
13
+ ```ruby
14
+ Connectors.configure do |config|
15
+ config.owner_class_name = "User"
16
+ config.current_owner_resolver = ->(controller) { controller.request.env["connectors.current_owner"] }
17
+ config.host_base_url = ENV.fetch("APP_BASE_URL")
18
+
19
+ # Must match the URI registered with the authorization server / CIMD document.
20
+ config.mcp.callback_url = ENV.fetch("MCP_CALLBACK_URL")
21
+ end
22
+ ```
23
+
24
+ Without an explicit MCP callback URL, the engine uses its mounted `/mcp/oauth/callback` endpoint under `host_base_url`. A split frontend/backend host can configure a frontend callback and POST the returned parameters to the engine callback endpoint. The callback request must resolve to the same owner that started authorization.
25
+
26
+ Only HTTPS destinations are allowed by default. Private, loopback and link-local addresses are refused after DNS resolution. The chosen address is pinned for the request while preserving TLS hostname verification. Redirects and environment proxies are not followed. Configure an explicit hostname in `config.mcp.private_hosts` for an intended private enterprise server; use `config.mcp.allow_loopback_http = true` only for deliberate local development/testing.
27
+
28
+ ## Create a connection
29
+
30
+ Use the existing credential endpoint, with `type: "mcp"`:
31
+
32
+ ```json
33
+ {
34
+ "type": "mcp",
35
+ "name": "My MCP server",
36
+ "data": {
37
+ "server_url": "https://server.example/mcp",
38
+ "auth_mode": "oauth"
39
+ }
40
+ }
41
+ ```
42
+
43
+ Supported credential fields:
44
+
45
+ | Field | Use |
46
+ | --- | --- |
47
+ | `server_url` | Exact remote MCP endpoint. |
48
+ | `auth_mode` | `none`, `bearer`, `headers`, or `oauth`. |
49
+ | `bearer_token` | Secret for `bearer` mode. |
50
+ | `headers` | Secret JSON object for custom header authentication. Transport-owned headers cannot be overridden. A custom `Authorization` header requires `headers` mode. |
51
+ | `authorization_server` | Optional issuer selection when the resource advertises multiple authorization servers. |
52
+ | `client_information` | Optional preregistered OAuth client: `client_id`, exact `issuer`, optional `client_secret`, and `token_endpoint_auth_method`. |
53
+
54
+ OAuth token/client state is internal. Credential APIs reject attempts to inject it. `include_data=true` returns only server URL, authentication mode and issuer selection for MCP connections. Editors can invoke shared connections; only owners can replace credentials or perform OAuth. Sharing grants access to the remote account represented by the grant.
55
+
56
+ For managed credentials, the existing secrets resolver can supply authentication fields. Configured client secrets are resolved at exchange/refresh time and are not duplicated into pending transactions or the token namespace. Changes to owner, destination, authentication fields or managed-secret reference invalidate pending work and token bindings.
57
+
58
+ ## ClickUp
59
+
60
+ ClickUp is a built-in named MCP connector. Create it without supplying an endpoint or secrets:
61
+
62
+ ```http
63
+ POST /connectors/credentials
64
+ Content-Type: application/json
65
+
66
+ {"type":"clickup","name":"ClickUp workspace"}
67
+ ```
68
+
69
+ Then POST `/connectors/credentials/:id/mcp/authorize`, open the returned `authorization_url` for the owner to consent and select Workspaces, and complete the shared MCP callback. The host must configure its callback URL and owner resolver as described above. ClickUp's advertised dynamic registration handles client registration; a preregistered client is optional. This does not use the separate ClickUp REST API OAuth configuration.
70
+
71
+ After authorization, GET `/connectors/credentials/:id/mcp/tools` and invoke a returned tool name through the shared call endpoint. The tool catalog and schemas come from the authorized server; the engine does not maintain a fixed ClickUp tool list. Permissions and remote rate limits remain controlled by ClickUp.
72
+
73
+ The named connector fixes the official endpoint to `https://mcp.clickup.com/mcp` and authentication to OAuth. API tokens, custom authentication headers and endpoint overrides are rejected, including values supplied by a managed-secret resolver. Owners can replace client registration, which clears previous authorization. Disconnect uses the shared local revocation behavior.
74
+
75
+ `GET /connectors/types/clickup` exposes `connector.mcp` metadata with endpoint, authentication mode, protocol version and grant-scoped URL templates. Replace `:id` with the created credential ID. `connector.redirect_uri` uses the same callback resolver as authorization. The generic MCP type exposes the same workflow metadata, with a user-configured endpoint and authentication mode.
76
+
77
+ **Evidence and validation (2026-09-22):** [ClickUp's official documentation](https://developer.clickup.com/docs/connect-an-ai-assistant-to-clickups-mcp-server) specifies the endpoint and OAuth-only authentication. Its live 401 challenge points to [protected-resource metadata](https://mcp.clickup.com/.well-known/oauth-protected-resource/mcp), advertising `read` and `write` scopes. Its [authorization-server metadata](https://mcp.clickup.com/.well-known/oauth-authorization-server) advertises S256 PKCE, dynamic registration, public-client token authentication and required callback issuer identification. It advertises `authorization_code` without `refresh_token`; registration requests respect that metadata. Endpoints and scopes are discovered at runtime, not copied into provider-specific OAuth code.
78
+
79
+ Request specs use captured public metadata and simulated registration/token/tool responses. The synthetic tool in those specs is explicitly a fixture. Live verification has covered public discovery and a demo owner completing consent with encrypted credential storage. Authenticated tool discovery/invocation against that Workspace has not been verified by the automated fixture suite.
80
+
81
+ ## Discover, invoke and resume
82
+
83
+ Paths below are relative to the engine mount, normally `/connectors`.
84
+
85
+ | Method and path | Behavior |
86
+ | --- | --- |
87
+ | `GET /credentials/:id/mcp/tools` | Returns `{ "tools": [...] }` with complete native descriptors and schemas. Requires viewer access. |
88
+ | `POST /credentials/:id/mcp/tools/call` | Body: `{ "name": "tool_name", "arguments": {} }`. Requires editor access. Returns the MCP tool result. |
89
+ | `POST /credentials/:id/mcp/interactions/resume` | Body: `{ "interaction_id": "...", "responses": { ... } }`. Requires the initiating actor's editor access. |
90
+ | `POST /credentials/:id/mcp/authorize` | Owner-only. Returns `authorization_url`. Accepts an optional `authorization_context` from a failed MCP request. |
91
+ | `GET` or `POST /mcp/oauth/callback` | Accepts `state`, `code`, `iss` or an OAuth `error`; completes a one-time authorization transaction. |
92
+ | `POST /credentials/:id/revoke` | Owner-only local disconnect: marks the grant revoked and clears stored authentication fields. It does not claim to revoke tokens at the remote authorization server. |
93
+
94
+ Arguments are validated without coercion or dropping extra fields. JSON Schema 2020-12 and draft-07 are supported; external schema references are disabled. Output schemas are checked. Content blocks and structured results, including arrays/scalars/null, are preserved. `isError: true` is a completed tool result, distinct from HTTP or protocol failure.
95
+
96
+ The Ruby application entry points use the same access policy as HTTP:
97
+
98
+ ```ruby
99
+ client = Connectors::MCP::Client.new(grant: grant, actor: current_owner)
100
+ client.tools
101
+ result = client.call_tool(name: "search", arguments: { "query" => "ruby" })
102
+ ```
103
+
104
+ Pass trusted `principals:` when the host uses team/workspace sharing. Create a client for the operation; do not share a mutable client instance across worker threads. Use `Client` and `Authorization` as application entry points; transport and schema classes are implementation helpers.
105
+
106
+ A failed call can return HTTP 401 with `error.type: "authorization_required"` and an encrypted `authorization_context`. Pass that context to the owner-only authorize endpoint so it retains the **actual failed operation's** scope challenge. After consent, retry the intended operation explicitly. Unrelated 403s are not treated as scope upgrades. Ruby callers can catch `Connectors::MCP::AuthorizationRequired` and pass its `challenge` to `Authorization#start`.
107
+
108
+ No tool call is automatically replayed after a timeout or broken stream. Its remote outcome may be unknown. A recognized authentication failure can trigger one coordinated refresh and one retry. Protocol errors retain their numeric code in Ruby but never trigger an automatic legacy downgrade.
109
+
110
+ ## OAuth registration and token handling
111
+
112
+ The engine follows protected-resource, OAuth authorization-server and OIDC discovery. It requires advertised S256 PKCE, binds client information to the selected issuer, validates callback issuer/state, and includes the MCP resource in authorization/token requests.
113
+
114
+ Registration preference is preregistration, advertised Client ID Metadata Documents (CIMD), then advertised Dynamic Client Registration (DCR). Otherwise the owner must provide client information. DCR is a compatibility mechanism, not assumed universally available.
115
+
116
+ To use CIMD, publish a public HTTPS JSON document with matching `client_id`, `client_name` and `redirect_uris`, then set `config.mcp.client_metadata_url`. The URL must identify that document. The engine validates it before use; it does not publish a document for your host automatically.
117
+
118
+ Token endpoint authentication supports `none`, `client_secret_basic`, `client_secret_post`, and host-signed `private_key_jwt`, subject to server metadata. For the last method, configure:
119
+
120
+ ```ruby
121
+ config.mcp.assertion_provider = ->(client_id, token_endpoint) {
122
+ # Return a short-lived JWT signed by the host's key manager with the required
123
+ # issuer, subject, audience, expiry and replay protection for this registration.
124
+ MyKeyManager.oauth_client_assertion(client_id: client_id, audience: token_endpoint)
125
+ }
126
+ ```
127
+
128
+ The host owns signing-key management; the engine does not invent a signing key or JWT policy. Requested scopes are retained separately from token response scopes. `offline_access` is requested only when advertised. Refresh rotation is serialized across processes; transient failures preserve credentials, while `invalid_grant` requires reconnect. Grants expose token expiry through their existing `expires_at` field.
129
+
130
+ ## Elicitation, subscriptions and cancellation
131
+
132
+ A host that implements the corresponding presentation can enable `config.mcp.elicitation_modes = ["form", "url"]`. The default is empty, and unadvertised modes are rejected.
133
+
134
+ An `input_required` result includes `interaction_id` and the server's input requests. The engine retains the original call and opaque state in encrypted storage. Present the request to the user, then resume with responses keyed by the server's input-request IDs. Form acceptance includes schema-valid `content`; URL acceptance, decline and cancel omit content. URL acceptance is consent to an external interaction, not proof that it completed. Never prefetch/open a URL automatically; show the requesting server and full URL, then open it only after user consent. Third-party credentials must stay outside this client.
135
+
136
+ Each resume is claimed once. A duplicate submission is rejected, not replayed. State-only continuations can be resumed with `{}`; hosts should back off rather than loop immediately. Limits bound rounds and expiry. This is not a guarantee of exactly-once execution at the remote server.
137
+
138
+ For on-demand tools-list notifications:
139
+
140
+ ```ruby
141
+ cancellation = Connectors::MCP::Cancellation.new
142
+ client = Connectors::MCP::Client.new(grant: grant, actor: current_owner, cancellation: cancellation)
143
+ client.subscribe { |notification| handle_mcp_notification(notification) }
144
+ # Another thread can call cancellation.cancel to close an idle stream.
145
+ ```
146
+
147
+ The stream validates acknowledgment, filter and correlation. It ends on cancellation, graceful completion, deadline, size limit or error. Hosts can start a fresh subscription after disconnection. The engine starts no background daemon and maintains no cross-request tool cache.
148
+
149
+ ## Limits, maintenance and supported scope
150
+
151
+ Defaults: 5-second connection timeout, 30-second request/authorization deadline, 300-second subscription deadline, 4 MiB request/response limit, 100 pages, 1,000 tools, 10 interaction rounds, 15-minute pending-state expiry, and a 1-second schema-validation deadline. Settings are available on `config.mcp`; configure positive bounds appropriate for the host. Error messages and filtered Rails parameters avoid exposing authentication material and tool input bodies.
152
+
153
+ Upstream HTTP 429 responses return HTTP 429 with `error.type: "rate_limited"`. Valid `Retry-After` guidance is exposed in the response header and `error.retry_after` as a string containing seconds or an HTTP date; missing or malformed guidance is omitted. Tool calls are not automatically replayed. Other upstream HTTP failures remain sanitized 502 responses.
154
+
155
+ Schedule `bundle exec rake connectors:mcp:cleanup` through the host's existing scheduler to delete expired pending records. There is no additional scheduler dependency.
156
+
157
+ This release targets **2026-07-28 remote tools**. It does not advertise historical session-based transports, local stdio execution, prompts/resources product APIs, sampling, roots, Tasks, Apps, client-credentials grant extensions or enterprise authorization extensions. Those are separate protocol features, not implicit support claims. `private_key_jwt` client authentication above is supported for user OAuth; that does not imply support for the separate machine-to-machine grant extension.
158
+
159
+ ## Validation
160
+
161
+ Validated on 2026-09-22 after packaging and documentation review: **543 examples, 0 failures** (random seed `17450`, frozen lockfile), **200 Ruby files with no RuboCop offenses**, and a strict gem build plus isolated installation/boot check. This includes the existing connector regression suite.
162
+
163
+ RSpec covers protocol/schema contracts, authentication/registration, access isolation, secret serialization, real PostgreSQL concurrency, durable resume, real chunked SSE and idle cancellation. Interoperability runs against:
164
+
165
+ 1. The official Ruby SDK 1.6.0 server over real HTTP.
166
+ 2. An independent Python stdlib fixture (`spec/support/mcp/oauth_server.py`, fixture version 1), covering public/bearer access and OAuth discovery, PKCE, callback completion in a separate Rails process, scope upgrade and refresh. Its test-only consent is automatic.
167
+
168
+ Run `bundle exec rspec --order random` and `bundle exec rubocop` after preparing an isolated PostgreSQL test database. Tests also need Python 3.9+ for the independent fixture. No production credentials or external account authorization is needed. These tests establish the documented integration behavior; they are not an official conformance certification or a claim that every hosted MCP provider has been tested.
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright Jackson Helio
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,146 @@
1
+ # Connectors
2
+
3
+ A mountable Rails engine for connecting accounts to provider APIs and remote MCP servers. It owns credential storage, authentication with providers, action execution, webhooks and polling. Your application owns user authentication, business workflows and scheduling.
4
+
5
+ **Status:** initial release series `0.1.x`. Check [RubyGems](https://rubygems.org/gems/connectors) for published versions. Package validation is separate from production acceptance for a particular host and provider.
6
+
7
+ ## Requirements
8
+
9
+ | Component | Requirement | Validation baseline |
10
+ | --- | --- | --- |
11
+ | Ruby | 3.2 or later | Ruby 3.4.8 locally and in the CI configuration |
12
+ | Rails | 8.1.3 or later in the 8.1 series | Rails 8.1.3 |
13
+ | Database | PostgreSQL 13+; migrations use UUIDs, JSONB and partial indexes | CI targets PostgreSQL 16 |
14
+ | Identity | Persisted owner model with UUID primary keys; sharing principals also use UUIDs | Dummy `Owner` model in tests |
15
+ | Encryption | Host-configured Active Record Encryption keys and stable Rails `secret_key_base` | Synthetic keys in tests |
16
+
17
+ The declared Ruby minimum matches Rails' requirement; it is not a claim that every Ruby/OS combination has been tested. SQLite, MySQL and integer owner IDs are not supported by the shipped migrations. Install the host's PostgreSQL adapter (`pg`) in its Gemfile.
18
+
19
+ ## Installation
20
+
21
+ For a published release, add to your host's Gemfile:
22
+
23
+ ```ruby
24
+ gem "connectors", "~> 0.1.0"
25
+ gem "pg"
26
+ ```
27
+
28
+ Then run `bundle install`. Before the first publication, or to test unreleased changes, use `gem "connectors", git: "https://github.com/jackhelio/connectors.git", ref: "<reviewed-commit-sha>"` instead, replacing the ref with an actual reviewed commit. Local development can use `gem "connectors", path: "../connectors"`. See [release checks](docs/releasing.md).
29
+
30
+ Mount the engine in `config/routes.rb`:
31
+
32
+ ```ruby
33
+ Rails.application.routes.draw do
34
+ mount Connectors::Engine => "/connectors"
35
+ end
36
+ ```
37
+
38
+ Configure encryption before saving credentials:
39
+
40
+ ```sh
41
+ bin/rails db:encryption:init
42
+ bin/rails credentials:edit
43
+ ```
44
+
45
+ Store the generated `active_record_encryption` entries in the target environment's Rails credentials, or configure the equivalent Rails settings from your secret store. Keep the keys stable across restarts and all application/worker instances; retain them when restoring encrypted data. See the [Rails encryption setup](https://guides.rubyonrails.org/active_record_encryption.html#setup).
46
+
47
+ Configure the host identity boundary in `config/initializers/connectors.rb`:
48
+
49
+ ```ruby
50
+ Connectors.configure do |config|
51
+ config.owner_class_name = "User" # an existing model with UUID IDs
52
+ config.host_base_url = ENV.fetch("APP_BASE_URL")
53
+ config.current_owner_resolver = lambda do |controller|
54
+ controller.request.env["connectors.current_owner"]
55
+ end
56
+ end
57
+ ```
58
+
59
+ Here, `connectors.current_owner` is a Rack environment entry that **your authenticated host middleware must populate** with the owner record. It is an integration example, not a built-in authentication mechanism. You can instead resolve through your existing trusted authentication middleware; for example, a Warden host can read `controller.request.env["warden"]&.user`. Never derive the trusted owner directly from a caller-supplied ID. Engine controllers inherit `ActionController::API`, so host `ApplicationController` methods and authentication callbacks are not inherited automatically.
60
+
61
+ Run the host migrations:
62
+
63
+ ```sh
64
+ bin/rails db:migrate
65
+ ```
66
+
67
+ The engine appends its migration directory; copying migrations is unnecessary. Owners must exist before creating grants. If building a new owner table, use `create_table :users, id: :uuid`; an existing integer-ID table requires a host schema decision before integration.
68
+
69
+ ## First connection
70
+
71
+ The installed gem ships these connector profiles:
72
+
73
+ | Key | Connection |
74
+ | --- | --- |
75
+ | `resend` | Resend REST API, API key |
76
+ | `gmail` | Gmail REST API, user OAuth2 |
77
+ | `mcp` | Configurable remote MCP server |
78
+ | `clickup` | ClickUp's official OAuth-only MCP server |
79
+
80
+ Slack, GitHub and Echo under `test/dummy` are test fixtures, not shipped connectors. Inspect `GET /connectors/types` for the installed catalog and each connector's credential fields and actions.
81
+
82
+ In a Rails console, create a Resend connection for an existing owner:
83
+
84
+ ```ruby
85
+ owner = User.find(ENV.fetch("CONNECTOR_OWNER_ID"))
86
+ grant = Connectors::Grant.create!(
87
+ owner: owner,
88
+ connector_key: "resend",
89
+ display_name: "Transactional email",
90
+ credentials: { "api_key" => ENV.fetch("RESEND_API_KEY") }
91
+ )
92
+ Connectors::CredentialTester.run(grant)
93
+ ```
94
+
95
+ The connection test calls Resend's domains endpoint. It requires a key that can list domains; a sending-only key can still send email but will fail this read test.
96
+
97
+ For an authenticated HTTP client, the equivalent creation request is:
98
+
99
+ ```http
100
+ POST /connectors/credentials
101
+ Content-Type: application/json
102
+
103
+ {"type":"resend","name":"Transactional email","data":{"api_key":"YOUR_API_KEY"}}
104
+ ```
105
+
106
+ Use the returned `id` for `GET /connectors/credentials/:id/actions` to discover actions. Calling an action executes a real provider operation:
107
+
108
+ ```http
109
+ POST /connectors/credentials/:id/actions/send_email
110
+ Content-Type: application/json
111
+
112
+ {"data":{"from":"sender@your-verified-domain.example","to":"recipient@example.com","subject":"Hello","text":"Hello from Connectors"}}
113
+ ```
114
+
115
+ The backend validates required action parameters, attaches credentials, calls the provider and returns `{ "status": "ok", "action": "send_email", "data": ... }` or a typed error. REST action validation checks required values and filters undeclared inputs; it is not general JSON Schema validation. Direct Ruby callers use `Connectors::ActionRunner.call(grant, :send_email, input)` and must select an authorized grant themselves; HTTP endpoints enforce sharing roles.
116
+
117
+ For Gmail, configure `config.oauth_credentials = { gmail: { client_id: ..., client_secret: ... } }` from your secret store and register the callback shown by the connector catalog with Google. The default callback is `<APP_BASE_URL>/connectors/gmail/callback`. Start with `GET /connectors/gmail/authorize.json`, open `authorize_url`, and complete consent. Without `grant_id`, authorization creates a new connection; passing it explicitly reconnects an owned connection.
118
+
119
+ For ClickUp, create `{"type":"clickup","name":"ClickUp workspace"}`, then use the shared MCP authorization and tool endpoints. [The MCP guide](MCP_CLIENT.md#clickup) covers consent, discovery, invocation and supported protocol features. Applications do not implement ClickUp's provider transport or authentication. REST actions and MCP tools currently have distinct API endpoints.
120
+
121
+ ## Operating the engine
122
+
123
+ - Configure a durable Active Job backend and run its workers for webhook delivery. The gem supplies jobs, not a queue service.
124
+ - Schedule polling with `Connectors::PollRunner.run(grant, instance_key: trigger_id)` for each independent consumer. The host supplies the stable key and handles downstream delivery.
125
+ - Configure a shared `Rails.cache` if using connector quotas across workers. The dummy application's null cache is for tests.
126
+ - Schedule `bundle exec rake connectors:mcp:cleanup` to remove expired authorization and interaction records. The engine does not start a scheduler.
127
+ - Shared viewers can inspect metadata; editors can execute actions. Only the owner role can retrieve regular decrypted credential `data` with `include_data=true`. MCP returns only public configuration, even to owners.
128
+ - Revoked grants are rejected before actions and HTTP attempts, including cached clients and retries. Already in-flight requests cannot be recalled. REST revocation requires a provider declaration; MCP disconnect is local and clears stored authentication.
129
+ - Regular HTTP clients retry transient 502/503/504 responses twice for GET, HEAD, OPTIONS, PUT and DELETE. POST is excluded from those retries; token refresh may retry once after authentication failure. MCP tool calls are not automatically replayed on transport failure or HTTP 429.
130
+
131
+ Host authentication, authorization of direct Ruby calls, encryption-key management, queue operations and provider permissions remain application responsibilities. [Architecture and contracts](docs/architecture.md) explains the boundaries and stored data.
132
+
133
+ ## Documentation
134
+
135
+ - [Framework and DSL reference](CONNECTORS_FRAMEWORK.md)
136
+ - [Architecture and execution flows](docs/architecture.md)
137
+ - [Adding a connector with tests](docs/adding-connectors.md)
138
+ - [Remote MCP and ClickUp](MCP_CLIENT.md)
139
+ - [HTTP API contract](openapi.yaml) — paths are relative to the engine mount
140
+ - [Development and contributing](CONTRIBUTING.md)
141
+ - [Release requirements and verification](docs/releasing.md)
142
+ - [Changelog and upgrade notes](CHANGELOG.md)
143
+
144
+ ## License
145
+
146
+ [MIT](MIT-LICENSE). The vendored MCP schema has its [upstream license](lib/connectors/mcp/protocol/LICENSE) and [source attribution](lib/connectors/mcp/protocol/README.md) included in the gem.
@@ -0,0 +1,13 @@
1
+ module Clickup
2
+ class Connector < Connectors::Connector
3
+ connector key: :clickup, auth: :api_key, base_url: nil, display_name: "ClickUp",
4
+ documentation_url: "https://developer.clickup.com/docs/connect-an-ai-assistant-to-clickups-mcp-server",
5
+ instructions: "Create the connection, then authorize your ClickUp account and select your Workspaces. ClickUp MCP requires OAuth; personal API tokens are not supported."
6
+
7
+ mcp server_url: "https://mcp.clickup.com/mcp", auth_mode: "oauth"
8
+
9
+ credentials do
10
+ field :client_information, type: "json", secret: true, display_name: "Pre-registered OAuth client (optional)"
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,114 @@
1
+ module Gmail
2
+ # Shared HTTP layer for every Gmail action. Mirrors n8n's
3
+ # `googleApiRequest` / `googleApiRequestAllItems` in
4
+ # `packages/nodes-base/nodes/Google/Gmail/GenericFunctions.ts` —
5
+ # one entry point that normalizes Google-specific errors into our
6
+ # `Connectors::ApiError` hierarchy with user-friendly messages,
7
+ # plus an auto-paginating variant for `*.list` endpoints.
8
+ module Api
9
+ module_function
10
+
11
+ # @param resource [String, Symbol] — "message" / "label" / "thread" / "draft".
12
+ # Used to compose human-readable error messages ("Message not found",
13
+ # "Label name already exists", "Invalid thread ID").
14
+ def request(client, method, path, body: nil, query: nil, resource: "resource")
15
+ response =
16
+ case method.to_sym
17
+ when :get, :delete
18
+ client.public_send(method, path, query)
19
+ when :post, :patch, :put
20
+ client.public_send(method, path, body || {}) do |req|
21
+ req.params.update(query) if query && !query.empty?
22
+ end
23
+ else
24
+ raise ArgumentError, "unsupported HTTP method: #{method.inspect}"
25
+ end
26
+
27
+ response.body
28
+ rescue Connectors::ApiError => e
29
+ raise translate(e, resource: resource)
30
+ end
31
+
32
+ # Walks `nextPageToken` until exhausted; concatenates the array at
33
+ # `property_name`. Matches n8n's signature so the porting is mechanical.
34
+ def request_all(client, property_name, method, path, body: nil, query: nil, resource: "resource", page_size: 100)
35
+ items = []
36
+ q = (query || {}).dup
37
+ q[:maxResults] = page_size
38
+
39
+ loop do
40
+ page = request(client, method, path, body: body, query: q, resource: resource)
41
+ Array(page[property_name.to_s]).each { |it| items << it }
42
+ token = page["nextPageToken"]
43
+ break if token.to_s.empty?
44
+ q[:pageToken] = token
45
+ end
46
+
47
+ items
48
+ end
49
+
50
+ # n8n's `googleApiRequest` rescue ladder — translated to our types.
51
+ # When a translation matches, we raise a new ApiError with the
52
+ # user-meaningful message; otherwise the original passes through.
53
+ def translate(error, resource:)
54
+ status = error.respond_to?(:status) ? error.status.to_i : 0
55
+ body = error.respond_to?(:body) ? error.body : nil
56
+ message = error.message.to_s
57
+ details = google_error_details(body)
58
+ reason = details[:message].to_s
59
+
60
+ case status
61
+ when 400
62
+ if reason.include?("Invalid id value")
63
+ raise Connectors::ApiError.new(
64
+ "Invalid #{resource} ID — Gmail IDs should look something like `182b676d244938bd`",
65
+ status: status, body: body
66
+ )
67
+ end
68
+ when 401
69
+ raise Connectors::AuthenticationFailed.new(reason.presence || message, status: status, body: body)
70
+ when 404
71
+ raise Connectors::ApiError.new(
72
+ "#{titlecase(resource)} not found",
73
+ status: status, body: body
74
+ )
75
+ when 409
76
+ # n8n parity: any 409 on the label resource is treated as a
77
+ # name collision — Gmail's API doesn't return 409 for any other
78
+ # reason on /users/me/labels.
79
+ if resource.to_s == "label"
80
+ raise Connectors::ApiError.new("Label name already exists", status: status, body: body)
81
+ end
82
+ when 429
83
+ raise Connectors::RateLimited.new(reason.presence || "Gmail rate-limit hit", status: status, body: body)
84
+ end
85
+
86
+ error
87
+ end
88
+
89
+ # ErrorNormalization supplies parsed JSON. Also accept raw strings from
90
+ # custom clients using the same translation helper.
91
+ def google_error_details(body)
92
+ hash =
93
+ case body
94
+ when Hash then body
95
+ when String then (JSON.parse(body) rescue {})
96
+ else {}
97
+ end
98
+
99
+ err = hash["error"]
100
+ case err
101
+ when Hash
102
+ { message: err["message"], status: err["status"], errors: Array(err["errors"]) }
103
+ when String
104
+ { message: err }
105
+ else
106
+ {}
107
+ end
108
+ end
109
+
110
+ def titlecase(s)
111
+ s.to_s.tr("_", " ").split(" ").map(&:capitalize).join(" ")
112
+ end
113
+ end
114
+ end