agent-well-known-express 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +234 -0
- package/package.json +39 -0
- package/src/index.d.ts +140 -0
- package/src/index.js +340 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Agent Discovery Protocol Contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
# agent-well-known-express
|
|
2
|
+
|
|
3
|
+
Express middleware that auto-generates [Agent Discovery Protocol](https://github.com/user/agent-discovery-protocol/tree/main/spec) endpoints so any AI agent can discover and use your API at runtime.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install agent-well-known-express
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
> **Peer dependency:** Express 4.x or 5.x must be installed in your project.
|
|
12
|
+
|
|
13
|
+
## Quick Start
|
|
14
|
+
|
|
15
|
+
```js
|
|
16
|
+
const express = require('express');
|
|
17
|
+
const { agentManifest } = require('agent-well-known-express');
|
|
18
|
+
|
|
19
|
+
const app = express();
|
|
20
|
+
|
|
21
|
+
app.use(agentManifest({
|
|
22
|
+
name: "Acme Email",
|
|
23
|
+
description: "Send and receive emails programmatically. Supports HTML content, attachments, and inbox management.",
|
|
24
|
+
base_url: "https://api.acme-email.com",
|
|
25
|
+
auth: {
|
|
26
|
+
type: "api_key",
|
|
27
|
+
header: "Authorization",
|
|
28
|
+
prefix: "Bearer",
|
|
29
|
+
setup_url: "https://acme-email.com/api-keys"
|
|
30
|
+
},
|
|
31
|
+
pricing: {
|
|
32
|
+
type: "freemium",
|
|
33
|
+
plans: [
|
|
34
|
+
{ name: "Free", price: "$0/mo", limits: "100 emails/day" },
|
|
35
|
+
{ name: "Pro", price: "$9/mo", limits: "Unlimited" }
|
|
36
|
+
],
|
|
37
|
+
plans_url: "https://acme-email.com/pricing"
|
|
38
|
+
},
|
|
39
|
+
capabilities: [
|
|
40
|
+
{
|
|
41
|
+
name: "send_email",
|
|
42
|
+
description: "Send an email to one or more recipients with subject, body (plain text or HTML), and optional attachments.",
|
|
43
|
+
handler: {
|
|
44
|
+
endpoint: "/v1/emails/send",
|
|
45
|
+
method: "POST"
|
|
46
|
+
},
|
|
47
|
+
parameters: [
|
|
48
|
+
{ name: "to", type: "string", required: true, description: "Recipient email address.", example: "alice@example.com" },
|
|
49
|
+
{ name: "subject", type: "string", required: true, description: "Email subject line.", example: "Hello" },
|
|
50
|
+
{ name: "body", type: "string", required: true, description: "Email body content.", example: "Hi there!" }
|
|
51
|
+
],
|
|
52
|
+
response_example: {
|
|
53
|
+
status: 200,
|
|
54
|
+
body: {
|
|
55
|
+
success: true,
|
|
56
|
+
data: { message_id: "msg_abc123", status: "sent" }
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
auth_scopes: ["email.send"],
|
|
60
|
+
rate_limits: { requests_per_minute: 60 }
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
name: "list_inbox",
|
|
64
|
+
description: "List recent emails in the authenticated user's inbox with optional filtering.",
|
|
65
|
+
handler: {
|
|
66
|
+
endpoint: "/v1/emails/inbox",
|
|
67
|
+
method: "GET"
|
|
68
|
+
},
|
|
69
|
+
parameters: [
|
|
70
|
+
{ name: "limit", type: "number", required: false, description: "Max emails to return.", example: 20 },
|
|
71
|
+
{ name: "sender", type: "string", required: false, description: "Filter by sender address.", example: "bob@example.com" }
|
|
72
|
+
],
|
|
73
|
+
auth_scopes: ["email.read"],
|
|
74
|
+
rate_limits: { requests_per_minute: 120 }
|
|
75
|
+
}
|
|
76
|
+
]
|
|
77
|
+
}));
|
|
78
|
+
|
|
79
|
+
app.listen(3000, () => console.log('Listening on :3000'));
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
This registers two endpoints automatically:
|
|
83
|
+
|
|
84
|
+
| Endpoint | Description |
|
|
85
|
+
|----------|-------------|
|
|
86
|
+
| `GET /.well-known/agent` | The service manifest (JSON) |
|
|
87
|
+
| `GET /.well-known/agent/capabilities/:name` | Detail for a single capability |
|
|
88
|
+
|
|
89
|
+
Both endpoints include `Access-Control-Allow-Origin: *` and `Cache-Control: public, max-age=3600` headers.
|
|
90
|
+
|
|
91
|
+
## Configuration Reference
|
|
92
|
+
|
|
93
|
+
### Top-level fields
|
|
94
|
+
|
|
95
|
+
| Field | Type | Required | Description |
|
|
96
|
+
|-------|------|----------|-------------|
|
|
97
|
+
| `name` | `string` | Yes | Human-readable service name. |
|
|
98
|
+
| `description` | `string` | Yes | 2-3 sentences describing the service. Written for an LLM to understand what this service does and when to use it. |
|
|
99
|
+
| `base_url` | `string` | Yes | Base URL for all API calls. |
|
|
100
|
+
| `auth` | `object` | Yes | Authentication configuration (see below). |
|
|
101
|
+
| `pricing` | `object` | No | Pricing information (see below). |
|
|
102
|
+
| `capabilities` | `array` | Yes | List of capability objects (see below). |
|
|
103
|
+
|
|
104
|
+
### Auth object
|
|
105
|
+
|
|
106
|
+
**OAuth 2.0:**
|
|
107
|
+
|
|
108
|
+
```js
|
|
109
|
+
{
|
|
110
|
+
type: "oauth2",
|
|
111
|
+
authorization_url: "https://auth.example.com/authorize",
|
|
112
|
+
token_url: "https://auth.example.com/token",
|
|
113
|
+
scopes: ["read", "write"]
|
|
114
|
+
}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
**API Key:**
|
|
118
|
+
|
|
119
|
+
```js
|
|
120
|
+
{
|
|
121
|
+
type: "api_key",
|
|
122
|
+
header: "Authorization", // optional, defaults to "Authorization"
|
|
123
|
+
prefix: "Bearer", // optional
|
|
124
|
+
setup_url: "https://example.com/api-keys" // optional
|
|
125
|
+
}
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
**None (public API):**
|
|
129
|
+
|
|
130
|
+
```js
|
|
131
|
+
{ type: "none" }
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
### Pricing object (optional)
|
|
135
|
+
|
|
136
|
+
```js
|
|
137
|
+
{
|
|
138
|
+
type: "free", // "free" | "freemium" | "paid"
|
|
139
|
+
plans: [
|
|
140
|
+
{ name: "Free", price: "$0/mo", limits: "1000 requests/day" }
|
|
141
|
+
],
|
|
142
|
+
plans_url: "https://example.com/pricing"
|
|
143
|
+
}
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
### Capability object
|
|
147
|
+
|
|
148
|
+
| Field | Type | Required | Description |
|
|
149
|
+
|-------|------|----------|-------------|
|
|
150
|
+
| `name` | `string` | Yes | Machine-readable identifier (snake_case). |
|
|
151
|
+
| `description` | `string` | Yes | 1-2 sentence description for LLM understanding. |
|
|
152
|
+
| `handler` | `object` | Yes | `{ endpoint: string, method: string }` — the real API route. |
|
|
153
|
+
| `parameters` | `array` | No | Parameter definitions (see below). |
|
|
154
|
+
| `request_example` | `object` | No | Complete request example. Auto-generated if omitted. |
|
|
155
|
+
| `response_example` | `object` | No | Complete response example. |
|
|
156
|
+
| `auth_scopes` | `string[]` | No | OAuth scopes needed for this capability. |
|
|
157
|
+
| `rate_limits` | `object` | No | Rate limiting info (e.g. `{ requests_per_minute: 60 }`). |
|
|
158
|
+
|
|
159
|
+
### Parameter object
|
|
160
|
+
|
|
161
|
+
| Field | Type | Required | Description |
|
|
162
|
+
|-------|------|----------|-------------|
|
|
163
|
+
| `name` | `string` | Yes | Parameter name. |
|
|
164
|
+
| `type` | `string` | Yes | Type: `string`, `number`, `boolean`, `object`, `string[]`, `object[]`. |
|
|
165
|
+
| `required` | `boolean` | Yes | Whether this parameter is required. |
|
|
166
|
+
| `description` | `string` | Yes | What this parameter does. |
|
|
167
|
+
| `example` | `any` | No | Example value (used in auto-generated request_example). |
|
|
168
|
+
|
|
169
|
+
## What Gets Generated
|
|
170
|
+
|
|
171
|
+
### Manifest (`GET /.well-known/agent`)
|
|
172
|
+
|
|
173
|
+
Returns a spec-v1.0 manifest with `detail_url` auto-populated for each capability:
|
|
174
|
+
|
|
175
|
+
```json
|
|
176
|
+
{
|
|
177
|
+
"spec_version": "1.0",
|
|
178
|
+
"name": "Acme Email",
|
|
179
|
+
"description": "Send and receive emails programmatically...",
|
|
180
|
+
"base_url": "https://api.acme-email.com",
|
|
181
|
+
"auth": { "type": "api_key", "header": "Authorization", "prefix": "Bearer", "setup_url": "..." },
|
|
182
|
+
"pricing": { "type": "freemium", "plans": [...], "plans_url": "..." },
|
|
183
|
+
"capabilities": [
|
|
184
|
+
{
|
|
185
|
+
"name": "send_email",
|
|
186
|
+
"description": "Send an email to one or more recipients...",
|
|
187
|
+
"detail_url": "/.well-known/agent/capabilities/send_email"
|
|
188
|
+
}
|
|
189
|
+
]
|
|
190
|
+
}
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
### Capability Detail (`GET /.well-known/agent/capabilities/:name`)
|
|
194
|
+
|
|
195
|
+
Returns everything an agent needs to call the API:
|
|
196
|
+
|
|
197
|
+
```json
|
|
198
|
+
{
|
|
199
|
+
"name": "send_email",
|
|
200
|
+
"description": "Send an email to one or more recipients...",
|
|
201
|
+
"endpoint": "/v1/emails/send",
|
|
202
|
+
"method": "POST",
|
|
203
|
+
"parameters": [
|
|
204
|
+
{ "name": "to", "type": "string", "description": "Recipient email address.", "required": true, "example": "alice@example.com" }
|
|
205
|
+
],
|
|
206
|
+
"request_example": {
|
|
207
|
+
"method": "POST",
|
|
208
|
+
"url": "https://api.acme-email.com/v1/emails/send",
|
|
209
|
+
"headers": { "Authorization": "Bearer {api_key}", "Content-Type": "application/json" },
|
|
210
|
+
"body": { "to": "alice@example.com", "subject": "Hello", "body": "Hi there!" }
|
|
211
|
+
},
|
|
212
|
+
"response_example": { "status": 200, "body": { "success": true, "data": { "message_id": "msg_abc123" } } },
|
|
213
|
+
"auth_scopes": ["email.send"],
|
|
214
|
+
"rate_limits": { "requests_per_minute": 60 }
|
|
215
|
+
}
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
If you omit `request_example` from a capability, the middleware generates one automatically from `base_url`, `handler`, `auth`, and the required parameters' example values.
|
|
219
|
+
|
|
220
|
+
## Startup Validation
|
|
221
|
+
|
|
222
|
+
The middleware validates your configuration on startup and logs warnings via `console.warn` for any issues (missing fields, invalid types, etc.). It does **not** throw or crash — your server will still start even with a misconfigured manifest.
|
|
223
|
+
|
|
224
|
+
## Spec
|
|
225
|
+
|
|
226
|
+
This middleware implements the [Agent Discovery Protocol Specification v1.0](https://github.com/user/agent-discovery-protocol/tree/main/spec).
|
|
227
|
+
|
|
228
|
+
## Registry
|
|
229
|
+
|
|
230
|
+
Register your service in the [Agent Discovery Registry](https://registry.agentdiscovery.dev) so AI agents can find it.
|
|
231
|
+
|
|
232
|
+
## License
|
|
233
|
+
|
|
234
|
+
MIT
|
package/package.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "agent-well-known-express",
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"description": "Express middleware to serve the Agent Discovery Protocol (/.well-known/agent) manifest and capability detail endpoints.",
|
|
5
|
+
"main": "src/index.js",
|
|
6
|
+
"types": "src/index.d.ts",
|
|
7
|
+
"files": [
|
|
8
|
+
"src/",
|
|
9
|
+
"LICENSE",
|
|
10
|
+
"README.md"
|
|
11
|
+
],
|
|
12
|
+
"keywords": [
|
|
13
|
+
"agent-discovery",
|
|
14
|
+
"well-known",
|
|
15
|
+
"express",
|
|
16
|
+
"middleware",
|
|
17
|
+
"ai-agent",
|
|
18
|
+
"mcp",
|
|
19
|
+
"service-discovery",
|
|
20
|
+
"agent-discovery-protocol"
|
|
21
|
+
],
|
|
22
|
+
"author": "",
|
|
23
|
+
"license": "MIT",
|
|
24
|
+
"repository": {
|
|
25
|
+
"type": "git",
|
|
26
|
+
"url": "https://github.com/user/agent-discovery-protocol",
|
|
27
|
+
"directory": "sdks/express"
|
|
28
|
+
},
|
|
29
|
+
"homepage": "https://github.com/user/agent-discovery-protocol/tree/main/sdks/express",
|
|
30
|
+
"bugs": {
|
|
31
|
+
"url": "https://github.com/user/agent-discovery-protocol/issues"
|
|
32
|
+
},
|
|
33
|
+
"peerDependencies": {
|
|
34
|
+
"express": "^4.0.0 || ^5.0.0"
|
|
35
|
+
},
|
|
36
|
+
"engines": {
|
|
37
|
+
"node": ">=14.0.0"
|
|
38
|
+
}
|
|
39
|
+
}
|
package/src/index.d.ts
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { Router } from 'express';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Auth configuration for the service manifest.
|
|
5
|
+
*/
|
|
6
|
+
export interface AuthOAuth2 {
|
|
7
|
+
type: 'oauth2';
|
|
8
|
+
authorization_url: string;
|
|
9
|
+
token_url: string;
|
|
10
|
+
scopes?: string[];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface AuthApiKey {
|
|
14
|
+
type: 'api_key';
|
|
15
|
+
header?: string;
|
|
16
|
+
prefix?: string;
|
|
17
|
+
setup_url?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface AuthNone {
|
|
21
|
+
type: 'none';
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export type AuthConfig = AuthOAuth2 | AuthApiKey | AuthNone;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* A pricing plan entry.
|
|
28
|
+
*/
|
|
29
|
+
export interface PricingPlan {
|
|
30
|
+
name: string;
|
|
31
|
+
price: string;
|
|
32
|
+
limits?: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Pricing configuration for the service manifest.
|
|
37
|
+
*/
|
|
38
|
+
export interface PricingConfig {
|
|
39
|
+
type: 'free' | 'freemium' | 'paid';
|
|
40
|
+
plans?: PricingPlan[];
|
|
41
|
+
plans_url?: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* A parameter definition for a capability.
|
|
46
|
+
*/
|
|
47
|
+
export interface ParameterConfig {
|
|
48
|
+
name: string;
|
|
49
|
+
type: string;
|
|
50
|
+
required: boolean;
|
|
51
|
+
description: string;
|
|
52
|
+
example?: any;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Handler configuration specifying the actual API endpoint.
|
|
57
|
+
*/
|
|
58
|
+
export interface HandlerConfig {
|
|
59
|
+
endpoint: string;
|
|
60
|
+
method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Rate limit configuration for a capability.
|
|
65
|
+
*/
|
|
66
|
+
export interface RateLimitConfig {
|
|
67
|
+
requests_per_minute?: number;
|
|
68
|
+
daily_limit?: number;
|
|
69
|
+
[key: string]: any;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Example request object following the spec.
|
|
74
|
+
*/
|
|
75
|
+
export interface RequestExample {
|
|
76
|
+
method: string;
|
|
77
|
+
url: string;
|
|
78
|
+
headers?: Record<string, string>;
|
|
79
|
+
body?: any;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Example response object following the spec.
|
|
84
|
+
*/
|
|
85
|
+
export interface ResponseExample {
|
|
86
|
+
status: number;
|
|
87
|
+
body?: any;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* A capability definition provided by the user.
|
|
92
|
+
*/
|
|
93
|
+
export interface CapabilityConfig {
|
|
94
|
+
/** Machine-readable identifier (snake_case). */
|
|
95
|
+
name: string;
|
|
96
|
+
/** 1-2 sentence description for LLM understanding. */
|
|
97
|
+
description: string;
|
|
98
|
+
/** The real API endpoint and HTTP method for this capability. */
|
|
99
|
+
handler: HandlerConfig;
|
|
100
|
+
/** Parameter definitions. */
|
|
101
|
+
parameters?: ParameterConfig[];
|
|
102
|
+
/** A complete request example. Auto-generated from parameters if omitted. */
|
|
103
|
+
request_example?: RequestExample;
|
|
104
|
+
/** A complete response example. */
|
|
105
|
+
response_example?: ResponseExample;
|
|
106
|
+
/** OAuth scopes needed for this capability. */
|
|
107
|
+
auth_scopes?: string[];
|
|
108
|
+
/** Rate limiting information. */
|
|
109
|
+
rate_limits?: RateLimitConfig;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Full service configuration passed to `agentManifest()`.
|
|
114
|
+
*/
|
|
115
|
+
export interface AgentManifestConfig {
|
|
116
|
+
/** Human-readable service name. */
|
|
117
|
+
name: string;
|
|
118
|
+
/** 2-3 sentence description of the service, written for an LLM. */
|
|
119
|
+
description: string;
|
|
120
|
+
/** Base URL for all API calls. */
|
|
121
|
+
base_url: string;
|
|
122
|
+
/** Authentication configuration. */
|
|
123
|
+
auth: AuthConfig;
|
|
124
|
+
/** Pricing information (optional). */
|
|
125
|
+
pricing?: PricingConfig;
|
|
126
|
+
/** List of capabilities the service exposes. */
|
|
127
|
+
capabilities: CapabilityConfig[];
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Create an Express Router that serves Agent Discovery Protocol endpoints.
|
|
132
|
+
*
|
|
133
|
+
* Registers:
|
|
134
|
+
* GET /.well-known/agent — service manifest (JSON)
|
|
135
|
+
* GET /.well-known/agent/capabilities/:name — capability detail (JSON)
|
|
136
|
+
*
|
|
137
|
+
* @param config Service configuration following the Agent Discovery Protocol spec v1.0.
|
|
138
|
+
* @returns An Express Router to mount with `app.use()`.
|
|
139
|
+
*/
|
|
140
|
+
export function agentManifest(config: AgentManifestConfig): Router;
|
package/src/index.js
ADDED
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* agent-well-known-express
|
|
5
|
+
*
|
|
6
|
+
* Express middleware that auto-generates Agent Discovery Protocol endpoints:
|
|
7
|
+
* GET /.well-known/agent — the service manifest
|
|
8
|
+
* GET /.well-known/agent/capabilities/:name — capability detail
|
|
9
|
+
*
|
|
10
|
+
* Spec: https://github.com/user/agent-discovery-protocol/tree/main/spec
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
// Validation
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
|
|
17
|
+
const VALID_AUTH_TYPES = ['oauth2', 'api_key', 'none'];
|
|
18
|
+
const VALID_METHODS = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'];
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Validate the user-supplied config and return an array of warning strings.
|
|
22
|
+
* Does NOT throw — issues are advisory so the server can still start.
|
|
23
|
+
*/
|
|
24
|
+
function validateConfig(config) {
|
|
25
|
+
const warnings = [];
|
|
26
|
+
|
|
27
|
+
if (!config) {
|
|
28
|
+
warnings.push('Config is missing or undefined.');
|
|
29
|
+
return warnings;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Top-level required fields
|
|
33
|
+
if (!config.name || typeof config.name !== 'string') {
|
|
34
|
+
warnings.push('Missing or invalid "name" (expected non-empty string).');
|
|
35
|
+
}
|
|
36
|
+
if (!config.description || typeof config.description !== 'string') {
|
|
37
|
+
warnings.push('Missing or invalid "description" (expected non-empty string).');
|
|
38
|
+
}
|
|
39
|
+
if (!config.base_url || typeof config.base_url !== 'string') {
|
|
40
|
+
warnings.push('Missing or invalid "base_url" (expected non-empty string).');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Auth
|
|
44
|
+
if (!config.auth || typeof config.auth !== 'object') {
|
|
45
|
+
warnings.push('Missing or invalid "auth" object.');
|
|
46
|
+
} else if (!config.auth.type || !VALID_AUTH_TYPES.includes(config.auth.type)) {
|
|
47
|
+
warnings.push(
|
|
48
|
+
'Invalid "auth.type". Expected one of: ' + VALID_AUTH_TYPES.join(', ') + '.'
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Capabilities
|
|
53
|
+
if (!Array.isArray(config.capabilities) || config.capabilities.length === 0) {
|
|
54
|
+
warnings.push('Missing or empty "capabilities" array.');
|
|
55
|
+
} else {
|
|
56
|
+
config.capabilities.forEach(function (cap, idx) {
|
|
57
|
+
var prefix = 'capabilities[' + idx + ']';
|
|
58
|
+
|
|
59
|
+
if (!cap.name || typeof cap.name !== 'string') {
|
|
60
|
+
warnings.push(prefix + ': Missing or invalid "name".');
|
|
61
|
+
}
|
|
62
|
+
if (!cap.description || typeof cap.description !== 'string') {
|
|
63
|
+
warnings.push(prefix + ': Missing or invalid "description".');
|
|
64
|
+
}
|
|
65
|
+
if (!cap.handler || typeof cap.handler !== 'object') {
|
|
66
|
+
warnings.push(prefix + ': Missing or invalid "handler" object.');
|
|
67
|
+
} else {
|
|
68
|
+
if (!cap.handler.endpoint || typeof cap.handler.endpoint !== 'string') {
|
|
69
|
+
warnings.push(prefix + ': Missing "handler.endpoint".');
|
|
70
|
+
}
|
|
71
|
+
if (!cap.handler.method || typeof cap.handler.method !== 'string') {
|
|
72
|
+
warnings.push(prefix + ': Missing "handler.method".');
|
|
73
|
+
} else if (!VALID_METHODS.includes(cap.handler.method.toUpperCase())) {
|
|
74
|
+
warnings.push(
|
|
75
|
+
prefix +
|
|
76
|
+
': Invalid "handler.method" (' +
|
|
77
|
+
cap.handler.method +
|
|
78
|
+
'). Expected one of: ' +
|
|
79
|
+
VALID_METHODS.join(', ') +
|
|
80
|
+
'.'
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Parameters
|
|
86
|
+
if (cap.parameters) {
|
|
87
|
+
if (!Array.isArray(cap.parameters)) {
|
|
88
|
+
warnings.push(prefix + ': "parameters" must be an array.');
|
|
89
|
+
} else {
|
|
90
|
+
cap.parameters.forEach(function (param, pIdx) {
|
|
91
|
+
var pPrefix = prefix + '.parameters[' + pIdx + ']';
|
|
92
|
+
if (!param.name || typeof param.name !== 'string') {
|
|
93
|
+
warnings.push(pPrefix + ': Missing or invalid "name".');
|
|
94
|
+
}
|
|
95
|
+
if (!param.type || typeof param.type !== 'string') {
|
|
96
|
+
warnings.push(pPrefix + ': Missing or invalid "type".');
|
|
97
|
+
}
|
|
98
|
+
if (typeof param.required !== 'boolean') {
|
|
99
|
+
warnings.push(pPrefix + ': Missing or invalid "required" (expected boolean).');
|
|
100
|
+
}
|
|
101
|
+
if (!param.description || typeof param.description !== 'string') {
|
|
102
|
+
warnings.push(pPrefix + ': Missing or invalid "description".');
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return warnings;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// ---------------------------------------------------------------------------
|
|
114
|
+
// Helpers
|
|
115
|
+
// ---------------------------------------------------------------------------
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Build the auth header representation used in auto-generated request examples.
|
|
119
|
+
*/
|
|
120
|
+
function buildAuthHeader(auth) {
|
|
121
|
+
if (!auth) return {};
|
|
122
|
+
switch (auth.type) {
|
|
123
|
+
case 'oauth2':
|
|
124
|
+
return { Authorization: 'Bearer {access_token}' };
|
|
125
|
+
case 'api_key': {
|
|
126
|
+
var header = auth.header || 'Authorization';
|
|
127
|
+
var prefix = auth.prefix ? auth.prefix + ' ' : '';
|
|
128
|
+
var obj = {};
|
|
129
|
+
obj[header] = prefix + '{api_key}';
|
|
130
|
+
return obj;
|
|
131
|
+
}
|
|
132
|
+
default:
|
|
133
|
+
return {};
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Auto-generate a request_example from the capability definition when the user
|
|
139
|
+
* has not provided one explicitly.
|
|
140
|
+
*/
|
|
141
|
+
function generateRequestExample(cap, config) {
|
|
142
|
+
var handler = cap.handler || {};
|
|
143
|
+
var method = (handler.method || 'GET').toUpperCase();
|
|
144
|
+
var baseUrl = (config.base_url || '').replace(/\/+$/, '');
|
|
145
|
+
var url = baseUrl + (handler.endpoint || '');
|
|
146
|
+
|
|
147
|
+
var headers = Object.assign(
|
|
148
|
+
{},
|
|
149
|
+
buildAuthHeader(config.auth),
|
|
150
|
+
{ 'Content-Type': 'application/json' }
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
var example = {
|
|
154
|
+
method: method,
|
|
155
|
+
url: url,
|
|
156
|
+
headers: headers,
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
// Build body from parameters using their example values
|
|
160
|
+
if (['POST', 'PUT', 'PATCH'].includes(method) && Array.isArray(cap.parameters)) {
|
|
161
|
+
var body = {};
|
|
162
|
+
cap.parameters.forEach(function (param) {
|
|
163
|
+
if (param.required && param.example !== undefined) {
|
|
164
|
+
body[param.name] = param.example;
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
if (Object.keys(body).length > 0) {
|
|
168
|
+
example.body = body;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return example;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// ---------------------------------------------------------------------------
|
|
176
|
+
// Manifest builder
|
|
177
|
+
// ---------------------------------------------------------------------------
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Build the spec-v1.0 manifest JSON from the user config.
|
|
181
|
+
*/
|
|
182
|
+
function buildManifest(config) {
|
|
183
|
+
var manifest = {
|
|
184
|
+
spec_version: '1.0',
|
|
185
|
+
name: config.name,
|
|
186
|
+
description: config.description,
|
|
187
|
+
base_url: config.base_url,
|
|
188
|
+
auth: config.auth,
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
if (config.pricing) {
|
|
192
|
+
manifest.pricing = config.pricing;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
manifest.capabilities = (config.capabilities || []).map(function (cap) {
|
|
196
|
+
var name = cap.name || '';
|
|
197
|
+
return {
|
|
198
|
+
name: name,
|
|
199
|
+
description: cap.description || '',
|
|
200
|
+
detail_url: '/.well-known/agent/capabilities/' + name,
|
|
201
|
+
};
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
return manifest;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Build the capability detail JSON for a single capability.
|
|
209
|
+
*/
|
|
210
|
+
function buildCapabilityDetail(cap, config) {
|
|
211
|
+
var handler = cap.handler || {};
|
|
212
|
+
var detail = {
|
|
213
|
+
name: cap.name || '',
|
|
214
|
+
description: cap.description || '',
|
|
215
|
+
endpoint: handler.endpoint || '',
|
|
216
|
+
method: (handler.method || 'GET').toUpperCase(),
|
|
217
|
+
parameters: (cap.parameters || []).map(function (p) {
|
|
218
|
+
var param = {
|
|
219
|
+
name: p.name,
|
|
220
|
+
type: p.type,
|
|
221
|
+
description: p.description,
|
|
222
|
+
required: p.required,
|
|
223
|
+
};
|
|
224
|
+
if (p.example !== undefined) {
|
|
225
|
+
param.example = p.example;
|
|
226
|
+
}
|
|
227
|
+
return param;
|
|
228
|
+
}),
|
|
229
|
+
request_example: cap.request_example || generateRequestExample(cap, config),
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
if (cap.response_example) {
|
|
233
|
+
detail.response_example = cap.response_example;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (cap.auth_scopes) {
|
|
237
|
+
detail.auth_scopes = cap.auth_scopes;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (cap.rate_limits) {
|
|
241
|
+
detail.rate_limits = cap.rate_limits;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
return detail;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// ---------------------------------------------------------------------------
|
|
248
|
+
// Middleware
|
|
249
|
+
// ---------------------------------------------------------------------------
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Create and return an Express Router that serves the Agent Discovery Protocol
|
|
253
|
+
* endpoints for the given service configuration.
|
|
254
|
+
*
|
|
255
|
+
* @param {object} config — service configuration (see README for full reference)
|
|
256
|
+
* @returns {import('express').Router}
|
|
257
|
+
*/
|
|
258
|
+
function agentManifest(config) {
|
|
259
|
+
// We require express at call-time so it's resolved from the host app's
|
|
260
|
+
// node_modules (peer dependency).
|
|
261
|
+
var express;
|
|
262
|
+
try {
|
|
263
|
+
express = require('express');
|
|
264
|
+
} catch (_err) {
|
|
265
|
+
throw new Error(
|
|
266
|
+
'agent-well-known-express: "express" is a peer dependency and must be installed in your project.'
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// --- Validate --------------------------------------------------------
|
|
271
|
+
var warnings = validateConfig(config);
|
|
272
|
+
if (warnings.length > 0) {
|
|
273
|
+
warnings.forEach(function (w) {
|
|
274
|
+
console.warn('[agent-well-known-express] ' + w);
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// --- Pre-compute manifest & detail map --------------------------------
|
|
279
|
+
var manifest = buildManifest(config);
|
|
280
|
+
var manifestJSON = JSON.stringify(manifest);
|
|
281
|
+
|
|
282
|
+
var capabilityDetails = {};
|
|
283
|
+
(config.capabilities || []).forEach(function (cap) {
|
|
284
|
+
capabilityDetails[cap.name] = buildCapabilityDetail(cap, config);
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
// --- Router -----------------------------------------------------------
|
|
288
|
+
var router = express.Router();
|
|
289
|
+
|
|
290
|
+
// CORS preflight for /.well-known/agent paths
|
|
291
|
+
router.options('/.well-known/agent', function (_req, res) {
|
|
292
|
+
res.set('Access-Control-Allow-Origin', '*');
|
|
293
|
+
res.set('Access-Control-Allow-Methods', 'GET, OPTIONS');
|
|
294
|
+
res.set('Access-Control-Allow-Headers', 'Content-Type');
|
|
295
|
+
res.status(204).end();
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
router.options('/.well-known/agent/capabilities/:name', function (_req, res) {
|
|
299
|
+
res.set('Access-Control-Allow-Origin', '*');
|
|
300
|
+
res.set('Access-Control-Allow-Methods', 'GET, OPTIONS');
|
|
301
|
+
res.set('Access-Control-Allow-Headers', 'Content-Type');
|
|
302
|
+
res.status(204).end();
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
// GET /.well-known/agent — the manifest
|
|
306
|
+
router.get('/.well-known/agent', function (_req, res) {
|
|
307
|
+
res.set('Content-Type', 'application/json');
|
|
308
|
+
res.set('Access-Control-Allow-Origin', '*');
|
|
309
|
+
res.set('Cache-Control', 'public, max-age=3600');
|
|
310
|
+
res.status(200).send(manifestJSON);
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
// GET /.well-known/agent/capabilities/:name — capability detail
|
|
314
|
+
router.get('/.well-known/agent/capabilities/:name', function (req, res) {
|
|
315
|
+
var name = req.params.name;
|
|
316
|
+
var detail = capabilityDetails[name];
|
|
317
|
+
|
|
318
|
+
if (!detail) {
|
|
319
|
+
res.set('Content-Type', 'application/json');
|
|
320
|
+
res.set('Access-Control-Allow-Origin', '*');
|
|
321
|
+
res.status(404).json({
|
|
322
|
+
error: 'Capability not found: ' + name,
|
|
323
|
+
});
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
res.set('Content-Type', 'application/json');
|
|
328
|
+
res.set('Access-Control-Allow-Origin', '*');
|
|
329
|
+
res.set('Cache-Control', 'public, max-age=3600');
|
|
330
|
+
res.status(200).json(detail);
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
return router;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// ---------------------------------------------------------------------------
|
|
337
|
+
// Exports
|
|
338
|
+
// ---------------------------------------------------------------------------
|
|
339
|
+
|
|
340
|
+
module.exports = { agentManifest };
|