hub_kernel-mcp 0.7.0 → 0.8.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 (33) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +148 -0
  3. data/app/controllers/hub_kernel/mcp/authorizations_controller.rb +69 -0
  4. data/app/controllers/hub_kernel/mcp/discovery_controller.rb +29 -0
  5. data/app/controllers/hub_kernel/mcp/registrations_controller.rb +28 -0
  6. data/app/controllers/hub_kernel/mcp/tokens_controller.rb +51 -0
  7. data/app/models/hub_kernel/mcp/authorization_code.rb +27 -0
  8. data/app/models/hub_kernel/mcp/client.rb +32 -0
  9. data/app/models/hub_kernel/mcp/connection.rb +50 -0
  10. data/app/models/hub_kernel/mcp/disconnect.rb +19 -0
  11. data/app/models/hub_kernel/mcp/prune.rb +12 -0
  12. data/app/views/hub_kernel/mcp/_connections.html.erb +10 -0
  13. data/app/views/hub_kernel/mcp/authorizations/new.html.erb +9 -0
  14. data/app/views/hub_kernel/mcp/authorizations/refused.html.erb +1 -0
  15. data/config/routes.rb +4 -0
  16. data/db/migrate/20261007000000_create_hub_kernel_mcp_clients.rb +10 -0
  17. data/db/migrate/20261008000000_create_hub_kernel_mcp_authorization_codes.rb +13 -0
  18. data/db/migrate/20261009000000_create_hub_kernel_mcp_connections.rb +11 -0
  19. data/db/migrate/20261009000001_add_refresh_token_to_hub_kernel_mcp_connections.rb +7 -0
  20. data/db/migrate/20261009000002_add_last_used_at_to_hub_kernel_mcp_connections.rb +5 -0
  21. data/db/migrate/20261009000003_add_code_digest_to_hub_kernel_mcp_connections.rb +6 -0
  22. data/db/migrate/20261009000004_add_registered_from_to_hub_kernel_mcp_clients.rb +6 -0
  23. data/lib/hub_kernel/mcp/challenge.rb +21 -0
  24. data/lib/hub_kernel/mcp/discovery.rb +12 -0
  25. data/lib/hub_kernel/mcp/engine.rb +4 -0
  26. data/lib/hub_kernel/mcp/version.rb +1 -1
  27. data/lib/hub_kernel/mcp.rb +14 -2
  28. data/lib/tasks/hub_kernel_mcp.rake +6 -0
  29. data/the_local/agents/hub_kernel-mcp-develop.md +370 -74
  30. data/the_local/agents/hub_kernel-mcp-info.md +133 -12
  31. data/the_local/agents/hub_kernel-mcp-install.md +284 -20
  32. data/the_local/interface.yml +45 -1
  33. metadata +23 -1
@@ -1,8 +1,8 @@
1
1
  ---
2
2
  name: hub_kernel-mcp-info
3
- description: Use to learn what hub_kernel-mcp offers — serving a host's hubs as MCP tools, how tools are named and scoped, and the vocabulary the install and develop locals assume.
3
+ description: Use to learn what hub_kernel-mcp offers — serving a host's hubs as MCP tools, how tools are named and scoped, how a client such as Claude's connector screen finds the endpoint's sign-in, registers itself, is approved by a person through the browser, trades its code for an access token and a refresh token and later trades the refresh token for a new pair, how each sign-in step refuses a bad request or an unexpected error, how many clients one address may register in an hour, how the host's sign-in finds the person a token acts for, how a person sees and disconnects the apps connected as them, how expired sign-in records are removed, and the vocabulary the install and develop locals assume.
4
4
  tools: Read
5
- scope: hub MCP tools — serving the methods every hub a host serves, from hub_kernel-interface's one served list, as MCP tools at one JSON-RPC endpoint in a host Rails app, each call behind the host's own sign-in and hub_kernel-interface's permission check and account scope, with a boot check for hubs that cannot be served as tools
5
+ scope: hub MCP tools — serving the methods every hub a host serves, from hub_kernel-interface's one served list, as MCP tools at one JSON-RPC endpoint in a host Rails app, each call behind the host's own sign-in and hub_kernel-interface's permission check and account scope, with a boot check for hubs that cannot be served as tools, the OAuth discovery documents, 401 challenge and client registration that let a client such as Claude's connector screen find the endpoint's sign-in and register by itself, with each client recording the address it registered from and a configurable limit on how many clients one address may register per hour past which registration is refused, and the approval page where a person signed in to the host through the browser approves that client and is issued an authorization code, the token exchange that trades that code with its PKCE verifier for an access token lasting an hour and a refresh token, the refresh exchange that trades a refresh token for a new access token and a new refresh token and retires the one posted, and the lookup a host's sign-in calls to get the person a bearer token acts for, which records when the connection was last used, and the settings section a host registers with settings_hub that lists a person's connections with the app's name, when it connected and when it was last used, and disconnects one that is the person's own, and the prune task a host schedules with its own job runner that removes expired authorization codes, connections whose access and refresh tokens have both expired, and clients over a day old with no connection
6
6
  ---
7
7
 
8
8
  This local explains hub_kernel-mcp and makes no changes.
@@ -20,24 +20,68 @@ wants an assistant to read or change that data on a person's behalf. Every call
20
20
  passes through the host's own sign-in first, then hub_kernel-interface's
21
21
  permission check and account scope, so a tool can do nothing the signed-in
22
22
  person could not already do. A boot check refuses to start a host whose served
23
- hubs cannot be turned into valid tool names.
23
+ hubs cannot be turned into valid tool names, or whose browser settings for the
24
+ approval page are not all set.
25
+
26
+ The gem also lets a client set itself up from the endpoint's address alone. A
27
+ refused request tells the client where the endpoint is described, two discovery
28
+ documents name the endpoint's sign-in and its registration address, and the
29
+ client registers itself there with its name and redirect addresses. The client
30
+ then sends the person to an approval page in the host, where they sign in
31
+ through the host's usual browser sign-in and approve or deny the client. An
32
+ approval gives the client an authorization code, which it trades at the token
33
+ address for an access token lasting an hour and a refresh token. When the
34
+ access token runs out, the client trades the refresh token at the same address
35
+ for a new pair, so the person does not approve the client again. The client
36
+ sends the access token on every request, and the host's sign-in asks the gem
37
+ which person the token acts for. A person using Claude's connector screen only
38
+ pastes the endpoint's address.
39
+
40
+ A person can see which apps are connected as them, and cut one off. The gem
41
+ ships a section for a host's settings page, registered through settings_hub,
42
+ that lists each of the person's connections with the app's name, when it
43
+ connected and when it was last used, and a Disconnect button for each. A
44
+ disconnected app's tokens stop working at once.
45
+
46
+ Registration needs no sign-in, so the gem keeps it and the records it leaves
47
+ bounded. One address may register only a set number of clients in an hour, ten
48
+ unless the host changes it. The gem also ships a prune task that removes
49
+ authorization codes, connections and clients that can no longer be used. The
50
+ gem does not run the task itself, so the host schedules it with its own job
51
+ runner.
24
52
 
25
53
  ## Interface
26
54
 
27
55
  This local declares no entry points of its own.
28
56
 
29
- - Adding the gem to a host, mounting the endpoint, configuring which controller
30
- and which person and account methods it uses, and running the boot check are
31
- owned by the install local, `hub_kernel-mcp-install`.
32
- - The endpoint itself and the MCP requests it answers are owned by the develop
33
- local, `hub_kernel-mcp-develop`.
57
+ - Adding the gem to a host, mounting the endpoint and the discovery documents,
58
+ installing the client, authorization code and connection tables, configuring
59
+ which controllers, person, account and sign-in methods and layout it uses,
60
+ setting the registration limit, running the boot check, calling the token
61
+ lookup from the host's sign-in, registering the connections section and its
62
+ disconnect action with settings_hub, and scheduling the prune task are owned
63
+ by the install local, `hub_kernel-mcp-install`.
64
+ - The endpoint itself, the MCP requests it answers, the discovery documents,
65
+ the challenge on a refused request, client registration, the approval page,
66
+ the token exchange, the refresh exchange, how each of them refuses, and the
67
+ query for a person's connections are owned by the develop local,
68
+ `hub_kernel-mcp-develop`.
34
69
 
35
70
  ## How to use it
36
71
 
37
- - To put hub_kernel-mcp into a Rails app, or to fix a host whose boot check
38
- fails, use `hub_kernel-mcp-install`.
39
- - To change what the endpoint answers, add support for another MCP request, or
40
- change how tools are listed, called or refused, use `hub_kernel-mcp-develop`.
72
+ - To put hub_kernel-mcp into a Rails app, to let a connector screen sign in to
73
+ it, to point the approval page at the host's browser sign-in and layout, to
74
+ make the host's sign-in accept the access tokens this gem issues, to add the
75
+ connected apps section to the host's settings page, to raise or lower how
76
+ many clients one address may register in an hour, to schedule the removal of
77
+ expired sign-in records, or to fix a host whose boot check fails, use
78
+ `hub_kernel-mcp-install`.
79
+ - To change what the endpoint answers, add support for another MCP request,
80
+ change how tools are listed, called or refused, change what the discovery
81
+ documents and registration say, change what the approval page checks and
82
+ shows, change when a code or a refresh token is traded for new tokens, change
83
+ how long either token lasts, change how a sign-in step refuses, or change
84
+ which connections count as a person's own, use `hub_kernel-mcp-develop`.
41
85
  - To decide which hubs are served at all, or to change permissions and account
42
86
  scope, work in hub_kernel-interface, which owns the served list and those
43
87
  checks.
@@ -64,3 +108,80 @@ This local declares no entry points of its own.
64
108
  as the same JSON-RPC error, `Unknown tool`, so a caller cannot tell the two
65
109
  apart. An unexpected error is reported to the host's error reporting and its
66
110
  message is never sent to the client.
111
+ - **Challenge** — the header added to every 401 the endpoint returns, pointing
112
+ the client at the document that describes the endpoint.
113
+ - **Discovery documents** — two OAuth documents served at the site root. The
114
+ resource document names the endpoint's address and its sign-in, which is the
115
+ site root. The sign-in document names the registration, approval and token
116
+ addresses under the endpoint's address, names the authorization code and
117
+ refresh grants as the ones it accepts, and requires PKCE with SHA-256 and no
118
+ client secret.
119
+ - **Client and registration** — a client is an app that registered itself by
120
+ posting its name and redirect addresses, and is answered with a client id.
121
+ Every redirect address must be HTTPS, or plain HTTP on the client's own
122
+ machine, and a registration with none or with any other address is refused
123
+ with the reason. A registration body that is not valid JSON is refused as
124
+ invalid client metadata.
125
+ - **Registration limit** — each client records the address it registered
126
+ from. Once one address has registered as many clients in the last hour as the
127
+ limit allows, ten by default, its next registration is refused with status
128
+ 429 and the reason, and no client is created.
129
+ - **Browser side** — the approval page runs on a host controller meant for
130
+ people in a browser, separate from the endpoint's controller. The host names
131
+ that controller, the method that makes a person sign in, the method that
132
+ returns the signed-in person, and the layout the page renders in.
133
+ - **Approval page** — shows which client wants to connect as the signed-in
134
+ person, with an approve and a deny button. A request missing its client id or
135
+ redirect address, naming a client that is not registered, or naming a
136
+ redirect address the client did not register gets an error page saying which,
137
+ and nothing is sent to any address. A request without a SHA-256 PKCE
138
+ challenge is sent back to the client as an invalid request. A denial is sent
139
+ back to the client as access denied.
140
+ - **Authorization code** — what an approval sends back to the client's redirect
141
+ address, along with the client's state. It is tied to the person, the client,
142
+ the redirect address and the PKCE challenge, lasts ten minutes, and only a
143
+ digest of it is stored.
144
+ - **Token exchange** — the client posts its client id, its code, its redirect
145
+ address and its PKCE verifier to the token address, with no sign-in. A code
146
+ that is unknown, expired, already traded, issued to another client, sent with
147
+ a different redirect address or with a verifier that does not match is refused
148
+ as an invalid grant. A code is traded once. A code posted a second time also
149
+ stops the access and refresh tokens already issued from it. A grant other than
150
+ the authorization code and refresh grants is refused as unsupported, and a
151
+ body that cannot be read is refused as an invalid request.
152
+ - **Access token, refresh token and connection** — what a traded code is
153
+ answered with: a bearer access token that lasts an hour and a refresh token
154
+ that lasts ninety days. The pair is stored as a connection tying the person
155
+ and the client to a digest of each token, so neither token itself is stored.
156
+ - **Refresh exchange** — the client posts its client id and its refresh token to
157
+ the token address, asking for the refresh grant, with no sign-in. It is
158
+ answered with a new access token and a new refresh token on the same
159
+ connection, and the new refresh token lasts another ninety days. The refresh
160
+ token posted and the access token it was issued with both stop working. A
161
+ refresh token that is unknown, already used, issued to another client, or
162
+ unused for ninety days is refused as an invalid grant, and of two refreshes
163
+ posting the same token at once only one succeeds.
164
+ - **Unexpected error at a sign-in step** — an error nobody planned for at the
165
+ registration address, the token address or a discovery document is answered
166
+ with status 500 and a server error. On the approval page it shows the error
167
+ page instead. Either way its message is never sent to the client or shown to
168
+ the person, and it is reported to the host's error reporting.
169
+ - **Token lookup** — what the host's sign-in calls with the bearer token from a
170
+ request. It gives the person the access token acts for while the token is
171
+ unexpired, and nothing for an expired, replaced or unknown token. Each lookup
172
+ that finds a person records the time on the connection as when it was last
173
+ used. The account a call is made in still comes from the host's account
174
+ method.
175
+ - **Connections section** — the part of a host's settings page that lists the
176
+ signed-in person's connections, oldest first, each with the app's name, when
177
+ it connected, and when it was last used or that it was never used. It is
178
+ given the person and the address its Disconnect buttons post to.
179
+ - **Disconnect** — the action a Disconnect button runs. It deletes the
180
+ connection, so both of its tokens stop working at once. A connection that is
181
+ not the person's own is left alone and the person is told it is not one of
182
+ theirs.
183
+ - **Prune** — the task that removes sign-in records nothing can use any more:
184
+ authorization codes past their ten minutes, connections whose access token
185
+ and refresh token have both expired, and clients registered over a day ago
186
+ that hold no connection and no authorization code. A connection with either
187
+ token still working is never removed. The gem never runs it on its own.
@@ -1,8 +1,8 @@
1
1
  ---
2
2
  name: hub_kernel-mcp-install
3
- description: Use to hook hub_kernel-mcp into a project — adding the gem, naming the controller the endpoint inherits from and the person and account methods on it, running the boot check after the served list is set, and mounting the engine.
3
+ description: Use to hook hub_kernel-mcp into a project — adding the gem, naming the controller the endpoint inherits from and the person and account methods on it, naming the browser controller, sign-in method, person method and layout the approval page runs with, running the boot check after the served list is set, mounting the engine, and, for sign-in from a connector screen, installing the migrations, mounting the discovery documents, setting how many clients one address may register per hour, having the host's sign-in look up the person an access token acts for, registering the settings section that lists a person's connections and disconnects one, and scheduling the task that prunes sign-in records.
4
4
  tools: Bash, Read, Edit
5
- scope: hub MCP tools — serving the methods every hub a host serves, from hub_kernel-interface's one served list, as MCP tools at one JSON-RPC endpoint in a host Rails app, each call behind the host's own sign-in and hub_kernel-interface's permission check and account scope, with a boot check for hubs that cannot be served as tools
5
+ scope: hub MCP tools — serving the methods every hub a host serves, from hub_kernel-interface's one served list, as MCP tools at one JSON-RPC endpoint in a host Rails app, each call behind the host's own sign-in and hub_kernel-interface's permission check and account scope, with a boot check for hubs that cannot be served as tools, the OAuth discovery documents, 401 challenge and client registration that let a client such as Claude's connector screen find the endpoint's sign-in and register by itself, with each client recording the address it registered from and a configurable limit on how many clients one address may register per hour past which registration is refused, and the approval page where a person signed in to the host through the browser approves that client and is issued an authorization code, the token exchange that trades that code with its PKCE verifier for an access token lasting an hour and a refresh token, the refresh exchange that trades a refresh token for a new access token and a new refresh token and retires the one posted, and the lookup a host's sign-in calls to get the person a bearer token acts for, which records when the connection was last used, and the settings section a host registers with settings_hub that lists a person's connections with the app's name, when it connected and when it was last used, and disconnects one that is the person's own, and the prune task a host schedules with its own job runner that removes expired authorization codes, connections whose access and refresh tokens have both expired, and clients over a day old with no connection
6
6
  ---
7
7
 
8
8
  This local follows these steps exactly and invents none. Where a step names a
@@ -11,9 +11,10 @@ decision, it asks the developer and does not pick.
11
11
  ## What hub_kernel-mcp is
12
12
 
13
13
  A Rails engine that serves every method the host's hubs expose as MCP tools at
14
- one JSON-RPC endpoint. Hook it in when the host already serves hubs through
15
- hub_kernel-interface and wants an MCP client to call them on a signed-in
16
- person's behalf.
14
+ one JSON-RPC endpoint, with a browser page where a signed-in person approves a
15
+ client such as Claude's connector screen. Hook it in when the host already
16
+ serves hubs through hub_kernel-interface and wants an MCP client to call them on
17
+ a signed-in person's behalf.
17
18
 
18
19
  ## Interface
19
20
 
@@ -22,7 +23,23 @@ person's behalf.
22
23
  `hub_kernel-interface` `~> 0.6`.
23
24
  - `mount HubKernel::Mcp::Engine` — mounts the endpoint in the host's
24
25
  `config/routes.rb` at the path given. The path answers POST with JSON-RPC and
25
- answers GET with status 405.
26
+ answers GET with status 405. A client registers at `<path>/register`, a person
27
+ approves it at `<path>/authorize`, the client trades the approval's code for an
28
+ access token and a refresh token at `<path>/token` and later trades the
29
+ refresh token there for a new pair, and every 401 the endpoint answers carries a
30
+ `WWW-Authenticate` header pointing at the discovery documents.
31
+ - `mount HubKernel::Mcp::Discovery` — mounts the two OAuth discovery documents
32
+ in the host's `config/routes.rb`. It must be mounted at `"/.well-known"`,
33
+ since the endpoint's 401 header names that path.
34
+ - `bin/rails hub_kernel_mcp:install:migrations` — copies the gem's seven
35
+ migrations into the host's `db/migrate`. They create the
36
+ `hub_kernel_mcp_clients` table, which holds each client that registers, the
37
+ `hub_kernel_mcp_authorization_codes` table, which holds each code the
38
+ approval page issues, and the `hub_kernel_mcp_connections` table, which holds
39
+ each access token issued for a code. They then add the refresh token's
40
+ columns, a `last_used_at` column and an indexed `code_digest` column to
41
+ `hub_kernel_mcp_connections`, and a `registered_from` column, indexed with
42
+ `created_at`, to `hub_kernel_mcp_clients`.
26
43
  - `HubKernel::Mcp.base_controller=` — the name, as a String, of the host
27
44
  controller the endpoint inherits from. Its before-actions, including sign-in,
28
45
  run before any hub is asked. Defaults to `"ActionController::API"`, which has
@@ -31,12 +48,56 @@ person's behalf.
31
48
  base controller that returns the person a request is made for. No default.
32
49
  - `HubKernel::Mcp.account_method=` — the name, as a Symbol, of the method on the
33
50
  base controller that returns the account a request is scoped to. No default.
51
+ - `HubKernel::Mcp.browser_controller=` — the name, as a String, of the host
52
+ controller the approval page inherits from. It must render HTML views, so it
53
+ is an `ActionController::Base` subclass. No default.
54
+ - `HubKernel::Mcp.sign_in_method=` — the name, as a Symbol, of the method on the
55
+ browser controller that sends a person who is not signed in through the
56
+ host's sign-in. The approval page calls it with no arguments before anything
57
+ else. No default.
58
+ - `HubKernel::Mcp.browser_person_method=` — the name, as a Symbol, of the method
59
+ on the browser controller that returns the signed-in person. That person must
60
+ be a record with a global id, since the issued code keeps the person by it.
61
+ No default.
62
+ - `HubKernel::Mcp.browser_layout=` — the name, as a String, of the host layout
63
+ the approval page is shown in. No default.
64
+ - `HubKernel::Mcp.registration_limit=` — the number, as an Integer, of clients
65
+ one IP address may register in the last hour. A registration past it is
66
+ answered with status 429 and `too_many_registrations`. Defaults to `10`.
34
67
  - `HubKernel::Mcp.check!` — the boot check. Raises
35
68
  `HubKernel::Mcp::UnservableHubError` when any served hub cannot be served as
36
- tools, and returns nothing otherwise.
69
+ tools, or when any of the four browser settings is not set, and returns
70
+ nothing otherwise.
37
71
  - `HubKernel::Mcp::UnservableHubError` — the error `check!` raises. Its message
38
72
  names every problem found, one per line. It is the same class as
39
73
  `HubKernel::Interface::UnservableHubError`, so rescuing either catches it.
74
+ - `HubKernel::Mcp::Connection.person_for` — takes the bearer token a client
75
+ sends, as a String, and returns the person who approved the client the token
76
+ was issued to. Returns `nil` for a token that is unknown, more than an hour
77
+ old, replaced by a refresh, or disconnected. Each time it returns a person it
78
+ records the time as the connection's last use. The host's sign-in on the base
79
+ controller calls it.
80
+ - `SettingsHub.section` — settings_hub's call that registers a section on the
81
+ host's settings page. The host calls it once to register the connections
82
+ section, naming the partial and the action below.
83
+ - `hub_kernel/mcp/connections` — the partial that lists the signed-in person's
84
+ connections, one per approved client, each with the app's name, when it
85
+ connected, when it was last used or `Never used`, and a Disconnect button. It
86
+ takes two locals: `person`, and `submit_url`, the address each button sends a
87
+ PATCH carrying `connection_id` to.
88
+ - `HubKernel::Mcp::Disconnect` — the action the Disconnect button runs. It is
89
+ built with `new(person:, account:, values:)`, where `values[:connection_id]`
90
+ names the connection, and `call` returns a result. The result's `ok?` is true
91
+ when the person's own connection was removed, and false with a `message` of
92
+ `That connection is not one of yours` otherwise. A removed connection's access
93
+ and refresh tokens stop working at once.
94
+ - `bin/rails hub_kernel_mcp:prune` — the task the host schedules. It runs
95
+ `HubKernel::Mcp::Prune.call` once and exits.
96
+ - `HubKernel::Mcp::Prune.call` — removes authorization codes past their ten
97
+ minutes, connections whose access token and refresh token have both expired,
98
+ and clients created more than a day ago that have no connection and no
99
+ authorization code. A connection whose access token or refresh token still
100
+ works is never removed. Takes no arguments and returns nothing the host uses.
40
101
 
41
102
  ## How to use it
42
103
 
@@ -65,20 +126,41 @@ person's behalf.
65
126
  on the base controller or a class it inherits from. If either is missing,
66
127
  ask what it should return before adding it.
67
128
 
68
- 4. Create `config/initializers/hub_kernel_mcp.rb` with the three answers:
129
+ 4. Ask the developer which host controller the approval page should inherit
130
+ from. It is the controller the host's browser pages use, usually
131
+ `ApplicationController`, and must be an `ActionController::Base` subclass.
132
+ Then ask for three names on it:
133
+
134
+ - the method that sends a person who is not signed in to the host's sign-in,
135
+ such as Devise's `:authenticate_user!`;
136
+ - the method that returns the signed-in person, such as `:current_user`;
137
+ - the layout the page is shown in, such as `"application"`.
138
+
139
+ All four are required, even when no connector screen will sign in: the boot
140
+ check names each one left unset, and the approval page's controller cannot
141
+ load without the browser controller. Do not choose any of them yourself.
142
+
143
+ 5. Create `config/initializers/hub_kernel_mcp.rb` with the answers from steps 2
144
+ to 4:
69
145
 
70
146
  ```ruby
71
147
  HubKernel::Mcp.base_controller = "Api::HubBaseController"
72
148
  HubKernel::Mcp.person_method = :current_person
73
149
  HubKernel::Mcp.account_method = :current_account
150
+
151
+ HubKernel::Mcp.browser_controller = "ApplicationController"
152
+ HubKernel::Mcp.sign_in_method = :authenticate_user!
153
+ HubKernel::Mcp.browser_person_method = :current_user
154
+ HubKernel::Mcp.browser_layout = "application"
74
155
  ```
75
156
 
76
157
  Set these in an initializer, not in `to_prepare`, so they are set before the
77
- endpoint's controller loads. The controller name is a String. Leaving
78
- `person_method` or `account_method` unset makes every `tools/list` and
79
- `tools/call` request answer with JSON-RPC error -32603, `Internal error`.
158
+ gem's controllers load. Both controller names and the layout are Strings, and
159
+ the three method names are Symbols. Leaving `person_method` or
160
+ `account_method` unset makes every `tools/list` and `tools/call` request
161
+ answer with JSON-RPC error -32603, `Internal error`.
80
162
 
81
- 5. Find where the host sets its served list, `HubKernel::Interface.hubs = [...]`,
163
+ 6. Find where the host sets its served list, `HubKernel::Interface.hubs = [...]`,
82
164
  inside `Rails.application.config.to_prepare`. Add `HubKernel::Mcp.check!` on
83
165
  the line after it, in the same block:
84
166
 
@@ -95,27 +177,209 @@ person's behalf.
95
177
  the developer that the hubs to serve are chosen in hub_kernel-interface
96
178
  first, and do not invent the list.
97
179
 
98
- 6. Ask the developer what path to mount the endpoint at, then add the mount to
180
+ 7. Ask the developer what path to mount the endpoint at, then add the mount to
99
181
  `config/routes.rb`:
100
182
 
101
183
  ```ruby
102
184
  mount HubKernel::Mcp::Engine => "/mcp"
103
185
  ```
104
186
 
187
+ 8. Ask the developer whether a client such as Claude's connector screen should
188
+ find the endpoint's sign-in, register and be approved by a person, so a
189
+ person only pastes the endpoint's address. If not, stop here and skip steps
190
+ 9 to 16.
191
+
192
+ 9. Copy the gem's migrations into the host and run them:
193
+
194
+ ```
195
+ bin/rails hub_kernel_mcp:install:migrations db:migrate
196
+ ```
197
+
198
+ This adds seven migrations to the host's `db/migrate` and the
199
+ `hub_kernel_mcp_clients`, `hub_kernel_mcp_authorization_codes` and
200
+ `hub_kernel_mcp_connections` tables to `db/schema.rb`. Commit all eight
201
+ files.
202
+
203
+ 10. Add the discovery mount to `config/routes.rb`, at the site root beside the
204
+ endpoint's mount, at exactly `"/.well-known"`:
205
+
206
+ ```ruby
207
+ mount HubKernel::Mcp::Engine => "/mcp"
208
+ mount HubKernel::Mcp::Discovery => "/.well-known"
209
+ ```
210
+
211
+ If the host already routes anything under `/.well-known`, show the developer
212
+ those routes and ask how to combine them before adding the mount.
213
+
214
+ 11. Ask the developer whether one IP address may register more or fewer than
215
+ ten clients an hour. Keep the default of `10` unless they name another
216
+ number, and if they do, add it to `config/initializers/hub_kernel_mcp.rb`:
217
+
218
+ ```ruby
219
+ HubKernel::Mcp.registration_limit = 20
220
+ ```
221
+
222
+ The limit counts clients by the address Rails reports as `request.remote_ip`.
223
+ If the host runs behind a proxy or load balancer that Rails does not trust,
224
+ every client is counted under the proxy's address and the limit is shared by
225
+ all of them. Ask the developer whether the host runs behind one, and if so
226
+ whether `request.remote_ip` already reports the caller's address, and do not
227
+ change the host's proxy settings yourself.
228
+
229
+ 12. Check how the base controller from step 2 refuses a caller who is not
230
+ signed in. A connector screen finds the sign-in only from a response with
231
+ status 401. If the controller redirects to a sign-in page or answers any
232
+ other status, tell the developer, and ask whether to change it to answer
233
+ 401 for this endpoint.
234
+
235
+ 13. Check whether the base controller's sign-in accepts the access token a
236
+ connector screen sends on every request, as `Authorization: Bearer <token>`.
237
+ The gem issues the token but does not sign anyone in with it, so the base
238
+ controller's person method must look the token up:
239
+
240
+ ```ruby
241
+ class Api::McpBaseController < ActionController::API
242
+ include ActionController::HttpAuthentication::Token::ControllerMethods
243
+
244
+ before_action { head :unauthorized unless current_person }
245
+
246
+ private
247
+
248
+ def current_person = authenticate_with_http_token { |token| HubKernel::Mcp::Connection.person_for(token) }
249
+ end
250
+ ```
251
+
252
+ If the base controller already signs callers in another way, such as with
253
+ an API key, ask the developer whether a bearer token from
254
+ `HubKernel::Mcp::Connection.person_for` should be accepted in addition to
255
+ it or in its place, and do not choose. A `nil` from `person_for` must end in
256
+ a 401, as in step 12. Then check that the account method from step 3 returns
257
+ an account for a person signed in this way, since every call is made in that
258
+ account. If it reads the account from something a token request does not
259
+ carry, such as a session or a subdomain, tell the developer and ask what it
260
+ should return.
261
+
262
+ 14. Check the layout from step 4. The approval page runs inside the gem's
263
+ engine, so a route helper the layout calls for one of the host's own routes,
264
+ such as `root_path`, must be written `main_app.root_path`. If the layout
265
+ calls any host route helper without `main_app.`, show the developer each one
266
+ and ask whether to prefix them or to name a different layout.
267
+
268
+ 15. Ask the developer whether a person should see the apps connected as them
269
+ and be able to disconnect one, and whether the host's settings page is
270
+ built with settings_hub. If they want the section and the host uses
271
+ settings_hub, register it where the host registers its other settings_hub
272
+ sections:
273
+
274
+ ```ruby
275
+ SettingsHub.section :connections, area: :user, title: "Connected apps",
276
+ renders: "hub_kernel/mcp/connections", runs: "HubKernel::Mcp::Disconnect"
277
+ ```
278
+
279
+ Ask the developer for the title, and do not change `area: :user`, since the
280
+ list is the signed-in person's own. If the host has no settings_hub section
281
+ registered anywhere yet, ask where sections are registered before adding
282
+ one. If the host does not use settings_hub, ask the developer where the list
283
+ should appear, then render the partial there with the signed-in person and
284
+ a host route that accepts a PATCH:
285
+
286
+ ```erb
287
+ <%= render "hub_kernel/mcp/connections", person: current_user, submit_url: connections_path %>
288
+ ```
289
+
290
+ That route's action builds
291
+ `HubKernel::Mcp::Disconnect.new(person: current_user, account: nil, values: { connection_id: params[:connection_id] })`,
292
+ calls `call`, and shows the result's `message` when `ok?` is false. Ask the
293
+ developer for the route's path and where to send the person afterwards, and
294
+ do not choose either.
295
+
296
+ 16. Schedule the prune task with the host's own job runner. The gem schedules
297
+ nothing, so without this step expired codes, unusable connections and
298
+ unused clients stay in the database. Ask the developer which job runner the
299
+ host uses, such as Solid Queue's recurring tasks, cron or the hosting
300
+ platform's scheduler, and how often the task should run, and do not choose
301
+ either. The scheduled entry runs one of:
302
+
303
+ ```
304
+ bin/rails hub_kernel_mcp:prune
305
+ ```
306
+
307
+ ```ruby
308
+ HubKernel::Mcp::Prune.call
309
+ ```
310
+
311
+ Use the rake task where the runner runs a shell command, and the Ruby call
312
+ where it runs a job or a line of Ruby inside the app. For Solid Queue, the
313
+ entry goes in the host's `config/recurring.yml`, with the environment and
314
+ schedule the developer named in place of the ones shown:
315
+
316
+ ```yaml
317
+ production:
318
+ prune_hub_kernel_mcp:
319
+ command: "HubKernel::Mcp::Prune.call"
320
+ schedule: every day at 3am
321
+ ```
322
+
323
+ If the host already has a recurring-task file, add the entry beside the
324
+ others and show the developer where it went.
325
+
105
326
  ## Conventions
106
327
 
107
328
  - After installing, boot the app or run `bin/rails runner "HubKernel::Mcp.check!"`.
108
329
  An `UnservableHubError` there lists every problem to fix, one per line: a
109
330
  problem hub_kernel-interface's own check finds, a served name holding two
110
331
  underscores in a row, a tool name holding a character other than a letter, a
111
- digit, an underscore or a hyphen, or a tool name longer than 64 characters.
112
- A tool name is `<served name>__<method>`.
113
- - Fix a check failure by changing the served name or the hub method in the host,
114
- never by removing the check. Inside `to_prepare` the check runs again after
115
- every code reload.
332
+ digit, an underscore or a hyphen, a tool name longer than 64 characters, or a
333
+ browser setting that is not set. A tool name is `<served name>__<method>`.
334
+ - Fix a check failure by changing the served name, the hub method or the
335
+ initializer in the host, never by removing the check. Inside `to_prepare` the
336
+ check runs again after every code reload.
116
337
  - Run the check again whenever the served list changes or a hub gains a method.
117
338
  - A change to `config/initializers/hub_kernel_mcp.rb` takes effect only after
118
339
  the app restarts.
340
+ - With discovery installed, check it with
341
+ `curl -i -X POST <host>/mcp` while signed out: the response should be 401 with
342
+ a `WWW-Authenticate` header naming
343
+ `<host>/.well-known/oauth-protected-resource/mcp`, and a GET to that address
344
+ should name the endpoint.
345
+ - With the approval page installed, open `<host>/mcp/authorize` in a browser
346
+ while signed out: the host's sign-in should take over. Signed in, the same
347
+ address with no query shows a page saying
348
+ `The approval request is missing client_id`.
349
+ - With the token lookup installed, check it with
350
+ `bin/rails runner "p HubKernel::Mcp::Connection.person_for('unknown')"`,
351
+ which should print `nil`, and with `curl -i -X POST <host>/mcp` carrying
352
+ `Authorization: Bearer unknown`, which should answer 401.
353
+ - Run `bin/rails hub_kernel_mcp:install:migrations` again after upgrading the
354
+ gem, then `db:migrate`. It copies only migrations the host does not have yet.
355
+ A host that installed an earlier version with fewer than seven migrations gets
356
+ the missing ones this way. Until they are run, the registration address or
357
+ the token address answers status 500 with `server_error`, or the token lookup
358
+ and the connections list fail on the missing `last_used_at` column.
359
+ - With the prune task scheduled, run `bin/rails hub_kernel_mcp:prune` once by
360
+ hand. It prints nothing and exits 0. It removes only codes past their ten
361
+ minutes, connections with no working token, and clients over a day old with
362
+ no connection or code, so running it more often than scheduled is safe.
363
+ - An unexpected error at the registration address, the token address, a
364
+ discovery document or the approval page is reported to the host's error
365
+ reporting through `Rails.error`, and its message is never sent to the client
366
+ or shown to the person. When a client reports `server_error` or the approval
367
+ page says `Signing in failed unexpectedly`, read the host's error reporting
368
+ for the cause.
369
+ - With the connections section installed, sign in and open the settings page:
370
+ each app approved as that person is listed, a connection whose token has not
371
+ been looked up since the `last_used_at` migration ran shows `Never used`, and
372
+ Disconnect removes the row. A request with that connection's access token
373
+ then answers 401.
374
+ - An access token lasts one hour. A refresh token lasts ninety days from when
375
+ it was issued, works only for the client it was issued to, and works once:
376
+ trading it retires it and the access token issued with it. A client that
377
+ lets its refresh token expire signs in again through the approval page.
378
+ - A token issued before the refresh token migration was run has no refresh
379
+ token, so its client signs in again through the approval page once the
380
+ access token expires.
119
381
  - Out of scope: choosing which hubs are served and setting permissions and
120
- account scope, which belong to hub_kernel-interface, and changing what the
121
- endpoint answers, which belongs to `hub_kernel-mcp-develop`.
382
+ account scope, which belong to hub_kernel-interface, building the settings
383
+ page itself, which belongs to settings_hub, and changing what the endpoint,
384
+ the discovery documents, the approval page or the connections section
385
+ answer, which belongs to `hub_kernel-mcp-develop`.
@@ -1,13 +1,26 @@
1
- scope: hub MCP tools — serving the methods every hub a host serves, from hub_kernel-interface's one served list, as MCP tools at one JSON-RPC endpoint in a host Rails app, each call behind the host's own sign-in and hub_kernel-interface's permission check and account scope, with a boot check for hubs that cannot be served as tools
1
+ scope: hub MCP tools — serving the methods every hub a host serves, from hub_kernel-interface's one served list, as MCP tools at one JSON-RPC endpoint in a host Rails app, each call behind the host's own sign-in and hub_kernel-interface's permission check and account scope, with a boot check for hubs that cannot be served as tools, the OAuth discovery documents, 401 challenge and client registration that let a client such as Claude's connector screen find the endpoint's sign-in and register by itself, with each client recording the address it registered from and a configurable limit on how many clients one address may register per hour past which registration is refused, and the approval page where a person signed in to the host through the browser approves that client and is issued an authorization code, the token exchange that trades that code with its PKCE verifier for an access token lasting an hour and a refresh token, the refresh exchange that trades a refresh token for a new access token and a new refresh token and retires the one posted, and the lookup a host's sign-in calls to get the person a bearer token acts for, which records when the connection was last used, and the settings section a host registers with settings_hub that lists a person's connections with the app's name, when it connected and when it was last used, and disconnects one that is the person's own, and the prune task a host schedules with its own job runner that removes expired authorization codes, connections whose access and refresh tokens have both expired, and clients over a day old with no connection
2
2
 
3
3
  install:
4
4
  - gem "hub_kernel-mcp"
5
5
  - mount HubKernel::Mcp::Engine
6
+ - mount HubKernel::Mcp::Discovery
7
+ - bin/rails hub_kernel_mcp:install:migrations
6
8
  - HubKernel::Mcp.base_controller=
7
9
  - HubKernel::Mcp.person_method=
8
10
  - HubKernel::Mcp.account_method=
11
+ - HubKernel::Mcp.browser_controller=
12
+ - HubKernel::Mcp.sign_in_method=
13
+ - HubKernel::Mcp.browser_person_method=
14
+ - HubKernel::Mcp.browser_layout=
15
+ - HubKernel::Mcp.registration_limit=
9
16
  - HubKernel::Mcp.check!
10
17
  - HubKernel::Mcp::UnservableHubError
18
+ - HubKernel::Mcp::Connection.person_for
19
+ - SettingsHub.section
20
+ - hub_kernel/mcp/connections
21
+ - HubKernel::Mcp::Disconnect
22
+ - bin/rails hub_kernel_mcp:prune
23
+ - HubKernel::Mcp::Prune.call
11
24
 
12
25
  develop:
13
26
  - POST /
@@ -15,10 +28,41 @@ develop:
15
28
  - ping
16
29
  - tools/list
17
30
  - tools/call
31
+ - POST /register
32
+ - GET /authorize
33
+ - POST /authorize
34
+ - POST /token
35
+ - grant_type=refresh_token
36
+ - WWW-Authenticate
37
+ - GET /.well-known/oauth-protected-resource
38
+ - GET /.well-known/oauth-authorization-server
39
+ - HubKernel::Mcp::Connection.of
18
40
 
19
41
  sources:
20
42
  - lib/hub_kernel/mcp.rb
21
43
  - lib/hub_kernel/mcp/engine.rb
44
+ - lib/hub_kernel/mcp/discovery.rb
45
+ - lib/hub_kernel/mcp/challenge.rb
22
46
  - config/routes.rb
47
+ - db/migrate/20261007000000_create_hub_kernel_mcp_clients.rb
48
+ - db/migrate/20261008000000_create_hub_kernel_mcp_authorization_codes.rb
49
+ - db/migrate/20261009000000_create_hub_kernel_mcp_connections.rb
50
+ - db/migrate/20261009000001_add_refresh_token_to_hub_kernel_mcp_connections.rb
51
+ - db/migrate/20261009000002_add_last_used_at_to_hub_kernel_mcp_connections.rb
52
+ - db/migrate/20261009000003_add_code_digest_to_hub_kernel_mcp_connections.rb
53
+ - db/migrate/20261009000004_add_registered_from_to_hub_kernel_mcp_clients.rb
54
+ - app/models/hub_kernel/mcp/client.rb
55
+ - app/models/hub_kernel/mcp/authorization_code.rb
56
+ - app/models/hub_kernel/mcp/connection.rb
57
+ - app/models/hub_kernel/mcp/disconnect.rb
58
+ - app/models/hub_kernel/mcp/prune.rb
59
+ - lib/tasks/hub_kernel_mcp.rake
23
60
  - app/controllers/hub_kernel/mcp/messages_controller.rb
61
+ - app/controllers/hub_kernel/mcp/discovery_controller.rb
62
+ - app/controllers/hub_kernel/mcp/registrations_controller.rb
63
+ - app/controllers/hub_kernel/mcp/authorizations_controller.rb
64
+ - app/controllers/hub_kernel/mcp/tokens_controller.rb
65
+ - app/views/hub_kernel/mcp/authorizations/new.html.erb
66
+ - app/views/hub_kernel/mcp/authorizations/refused.html.erb
67
+ - app/views/hub_kernel/mcp/_connections.html.erb
24
68
  - README.md