@wrongstack/requirement-intake-mcp 0.299.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.
- package/LICENSE +21 -0
- package/README.md +56 -0
- package/dist/adapter.d.ts +19 -0
- package/dist/cli.d.ts +15 -0
- package/dist/cli.js +409 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +255 -0
- package/dist/policy.d.ts +18 -0
- package/dist/version.d.ts +5 -0
- package/package.json +53 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 ECOSTACK TECHNOLOGY OÜ
|
|
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,56 @@
|
|
|
1
|
+
# @wrongstack/requirement-intake-mcp
|
|
2
|
+
|
|
3
|
+
WrongStack Requirements Intake as a project-scoped MCP server. Agents (Claude
|
|
4
|
+
Code, Codex, Cursor, custom MCP clients) can list intake records and file +
|
|
5
|
+
submit new requirement intakes directly into the project's intake store.
|
|
6
|
+
|
|
7
|
+
Mirrors the `@wrongstack/kanban-mcp` pattern: a standalone server with a
|
|
8
|
+
read-only default and an explicit `--writable` tier.
|
|
9
|
+
|
|
10
|
+
## Tools
|
|
11
|
+
|
|
12
|
+
| Tool | Tier | Purpose |
|
|
13
|
+
|---|---|---|
|
|
14
|
+
| `requirement_intake_list` | read (always) | List intake records for the project, newest first, optional `statuses` filter |
|
|
15
|
+
| `requirement_intake_submit` | writable (`--writable`) | File + submit an intake record from the `request` text (preserved verbatim) |
|
|
16
|
+
|
|
17
|
+
`requirement_intake_submit` arguments: `request` (required), `title`,
|
|
18
|
+
`requestType` (feature/bug_fix/refactor/… — unknown values normalize to
|
|
19
|
+
`other`/`unspecified`), `priority`, `idempotencyKey` (safe retries).
|
|
20
|
+
|
|
21
|
+
Records are stored under the project state dir
|
|
22
|
+
(`~/.wrongstack/projects/<slug>/requirement-intakes`) and are the same records
|
|
23
|
+
served by the WebUI REST API and the CLI `/intake` command. The MCP transport
|
|
24
|
+
is the authorization boundary; the service runs with an allow-all authorizer
|
|
25
|
+
inside it.
|
|
26
|
+
|
|
27
|
+
## Usage
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
# stdio (default) — read-only
|
|
31
|
+
wstack-requirement-intake-mcp --project-root /path/to/project
|
|
32
|
+
|
|
33
|
+
# expose intake filing
|
|
34
|
+
wstack-requirement-intake-mcp --project-root /path/to/project --writable
|
|
35
|
+
|
|
36
|
+
# HTTP transport with token auth
|
|
37
|
+
WRONGSTACK_MCP_TOKEN=secret wstack-requirement-intake-mcp \
|
|
38
|
+
--project-root /path/to/project --http --writable
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Project identity comes from `.wrongstack/project.json`
|
|
42
|
+
(`readProjectIdentity`). `requirement_intake_submit` creates it when missing;
|
|
43
|
+
`requirement_intake_list` requires it (run `wstack init` first).
|
|
44
|
+
|
|
45
|
+
## Development
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
pnpm exec vitest run packages/requirement-intake-mcp/tests
|
|
49
|
+
pnpm --filter @wrongstack/requirement-intake-mcp build
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Code references
|
|
53
|
+
|
|
54
|
+
- `src/adapter.ts` — `createRequirementIntakeMcpToolHost` / `...Server`
|
|
55
|
+
- `src/policy.ts` — read/writable capability tiers
|
|
56
|
+
- `src/cli.ts` — `wstack-requirement-intake-mcp` entry
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { MCPServer, type MCPServerToolHost } from '@wrongstack/mcp';
|
|
2
|
+
import { RequirementIntakeService } from '@wrongstack/requirement-intake';
|
|
3
|
+
import { type RequirementIntakeMcpPolicyOptions } from './policy.js';
|
|
4
|
+
export interface RequirementIntakeMcpDependencies {
|
|
5
|
+
/** Pre-built intake service (tests/embeds). Defaults to a fresh per-project service. */
|
|
6
|
+
service?: RequirementIntakeService | undefined;
|
|
7
|
+
/**
|
|
8
|
+
* Resolve the canonical project id. Defaults to reading `.wrongstack/project.json`
|
|
9
|
+
* (`readProjectIdentity`); when missing, `createIfMissing` (submit) creates it.
|
|
10
|
+
*/
|
|
11
|
+
resolveProjectId?: ((createIfMissing: boolean) => Promise<string>) | undefined;
|
|
12
|
+
}
|
|
13
|
+
export interface RequirementIntakeMcpToolHostOptions extends RequirementIntakeMcpPolicyOptions {
|
|
14
|
+
actor?: string | undefined;
|
|
15
|
+
dependencies?: RequirementIntakeMcpDependencies | undefined;
|
|
16
|
+
}
|
|
17
|
+
export declare function createRequirementIntakeMcpToolHost(projectRoot: string, opts?: RequirementIntakeMcpToolHostOptions): MCPServerToolHost;
|
|
18
|
+
export declare function createRequirementIntakeMcpServer(projectRoot: string, opts?: RequirementIntakeMcpToolHostOptions): MCPServer;
|
|
19
|
+
//# sourceMappingURL=adapter.d.ts.map
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
export interface ParsedArgs {
|
|
3
|
+
projectRoot: string;
|
|
4
|
+
transport: 'stdio' | 'http';
|
|
5
|
+
httpPort: number;
|
|
6
|
+
httpHost: string;
|
|
7
|
+
httpToken?: string | undefined;
|
|
8
|
+
writable: boolean;
|
|
9
|
+
actor?: string | undefined;
|
|
10
|
+
help: boolean;
|
|
11
|
+
}
|
|
12
|
+
export declare function printHelp(stdout: NodeJS.WriteStream): void;
|
|
13
|
+
export declare function parseArgs(argv: readonly string[], env?: NodeJS.ProcessEnv): ParsedArgs;
|
|
14
|
+
export declare function main(argv?: string[]): Promise<number>;
|
|
15
|
+
//# sourceMappingURL=cli.d.ts.map
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,409 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/cli.ts
|
|
4
|
+
import { realpathSync } from "node:fs";
|
|
5
|
+
import * as path from "node:path";
|
|
6
|
+
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
7
|
+
import { canonicalProjectRoot } from "@wrongstack/core/utils";
|
|
8
|
+
import { serveHttp, serveStdio } from "@wrongstack/mcp";
|
|
9
|
+
|
|
10
|
+
// src/adapter.ts
|
|
11
|
+
import {
|
|
12
|
+
ensureProjectIdentity,
|
|
13
|
+
readProjectIdentity,
|
|
14
|
+
resolveWstackPaths
|
|
15
|
+
} from "@wrongstack/core/utils";
|
|
16
|
+
import {
|
|
17
|
+
MCPServer
|
|
18
|
+
} from "@wrongstack/mcp";
|
|
19
|
+
import {
|
|
20
|
+
AllowAllIntakeAuthorizer,
|
|
21
|
+
INTAKE_PRIORITIES,
|
|
22
|
+
INTAKE_STATUSES,
|
|
23
|
+
RequirementIntakeService,
|
|
24
|
+
RequirementIntakeStore
|
|
25
|
+
} from "@wrongstack/requirement-intake";
|
|
26
|
+
|
|
27
|
+
// src/policy.ts
|
|
28
|
+
function selectRequirementIntakeTools(opts = {}) {
|
|
29
|
+
const tools = [{ name: "requirement_intake_list" }];
|
|
30
|
+
if (opts.writable === true) {
|
|
31
|
+
tools.push({ name: "requirement_intake_submit" });
|
|
32
|
+
}
|
|
33
|
+
return tools;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// src/version.ts
|
|
37
|
+
import { readFileSync } from "node:fs";
|
|
38
|
+
import { dirname, resolve } from "node:path";
|
|
39
|
+
import { fileURLToPath } from "node:url";
|
|
40
|
+
var here = dirname(fileURLToPath(import.meta.url));
|
|
41
|
+
var packagePath = resolve(here, "..", "package.json");
|
|
42
|
+
var cached;
|
|
43
|
+
function readServerInfo() {
|
|
44
|
+
if (cached) return cached;
|
|
45
|
+
try {
|
|
46
|
+
const pkg = JSON.parse(readFileSync(packagePath, "utf8"));
|
|
47
|
+
cached = {
|
|
48
|
+
name: pkg.name ?? "@wrongstack/requirement-intake-mcp",
|
|
49
|
+
version: pkg.version ?? "0.0.0"
|
|
50
|
+
};
|
|
51
|
+
} catch {
|
|
52
|
+
cached = { name: "@wrongstack/requirement-intake-mcp", version: "0.0.0" };
|
|
53
|
+
}
|
|
54
|
+
return cached;
|
|
55
|
+
}
|
|
56
|
+
var SERVER_INFO = readServerInfo();
|
|
57
|
+
|
|
58
|
+
// src/adapter.ts
|
|
59
|
+
var SUBMIT_SCHEMA = {
|
|
60
|
+
type: "object",
|
|
61
|
+
properties: {
|
|
62
|
+
request: {
|
|
63
|
+
type: "string",
|
|
64
|
+
description: "The exact software development request to record \u2014 a feature, bug fix, refactor, UI/API/infra change, migration, documentation, etc. Preserved verbatim."
|
|
65
|
+
},
|
|
66
|
+
title: {
|
|
67
|
+
type: "string",
|
|
68
|
+
description: "Optional short title. Defaults to a deterministic title from the request."
|
|
69
|
+
},
|
|
70
|
+
requestType: {
|
|
71
|
+
type: "string",
|
|
72
|
+
enum: [
|
|
73
|
+
"feature",
|
|
74
|
+
"bug_fix",
|
|
75
|
+
"refactor",
|
|
76
|
+
"performance",
|
|
77
|
+
"security",
|
|
78
|
+
"ui_change",
|
|
79
|
+
"api_change",
|
|
80
|
+
"infrastructure",
|
|
81
|
+
"migration",
|
|
82
|
+
"testing",
|
|
83
|
+
"documentation",
|
|
84
|
+
"maintenance",
|
|
85
|
+
"other",
|
|
86
|
+
"unspecified"
|
|
87
|
+
],
|
|
88
|
+
description: "Request type hint. Unknown values normalize to other/unspecified."
|
|
89
|
+
},
|
|
90
|
+
priority: {
|
|
91
|
+
type: "string",
|
|
92
|
+
enum: [...INTAKE_PRIORITIES],
|
|
93
|
+
description: "Desired priority."
|
|
94
|
+
},
|
|
95
|
+
idempotencyKey: {
|
|
96
|
+
type: "string",
|
|
97
|
+
description: "Optional key making create idempotent \u2014 retries return the existing record."
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
required: ["request"],
|
|
101
|
+
additionalProperties: false
|
|
102
|
+
};
|
|
103
|
+
var LIST_SCHEMA = {
|
|
104
|
+
type: "object",
|
|
105
|
+
properties: {
|
|
106
|
+
statuses: {
|
|
107
|
+
type: "array",
|
|
108
|
+
items: { type: "string", enum: [...INTAKE_STATUSES] },
|
|
109
|
+
description: "Optional status filter (draft, collecting_information, submitted, cancelled, archived)."
|
|
110
|
+
}
|
|
111
|
+
},
|
|
112
|
+
additionalProperties: false
|
|
113
|
+
};
|
|
114
|
+
var TOOL_DESCRIPTIONS = {
|
|
115
|
+
requirement_intake_list: "List requirement intake records for the project, newest first, optionally filtered by status.",
|
|
116
|
+
requirement_intake_submit: "File and submit a requirement intake record from the given request text. Requires server --writable."
|
|
117
|
+
};
|
|
118
|
+
function toolDescriptor(name) {
|
|
119
|
+
return {
|
|
120
|
+
name,
|
|
121
|
+
description: TOOL_DESCRIPTIONS[name],
|
|
122
|
+
inputSchema: name === "requirement_intake_submit" ? SUBMIT_SCHEMA : LIST_SCHEMA
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
function intakeContext(projectId, actor) {
|
|
126
|
+
return { id: actor, type: "automation", projectId };
|
|
127
|
+
}
|
|
128
|
+
async function defaultResolveProjectId(projectRoot, createIfMissing) {
|
|
129
|
+
const existing = await readProjectIdentity(projectRoot);
|
|
130
|
+
if (existing) return existing.projectId;
|
|
131
|
+
if (!createIfMissing) {
|
|
132
|
+
throw new Error(
|
|
133
|
+
"No WrongStack project identity found \u2014 run `wstack init` (or file an intake first) to create it"
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
return (await ensureProjectIdentity(projectRoot)).identity.projectId;
|
|
137
|
+
}
|
|
138
|
+
function createRequirementIntakeMcpToolHost(projectRoot, opts = {}) {
|
|
139
|
+
const policy = selectRequirementIntakeTools(opts);
|
|
140
|
+
const allowed = new Set(policy.map((entry) => entry.name));
|
|
141
|
+
const actor = opts.actor?.trim() || "external-intake-mcp";
|
|
142
|
+
const service = opts.dependencies?.service ?? new RequirementIntakeService({
|
|
143
|
+
store: new RequirementIntakeStore({
|
|
144
|
+
baseDir: resolveWstackPaths({ projectRoot }).projectRequirementIntakes
|
|
145
|
+
}),
|
|
146
|
+
authorizer: new AllowAllIntakeAuthorizer()
|
|
147
|
+
});
|
|
148
|
+
const resolveProjectId = opts.dependencies?.resolveProjectId ?? ((createIfMissing) => defaultResolveProjectId(projectRoot, createIfMissing));
|
|
149
|
+
return {
|
|
150
|
+
listTools() {
|
|
151
|
+
return policy.map((entry) => toolDescriptor(entry.name));
|
|
152
|
+
},
|
|
153
|
+
async callTool(name, args) {
|
|
154
|
+
if (!allowed.has(name)) {
|
|
155
|
+
return {
|
|
156
|
+
content: `Tool "${name}" is not exposed by this Requirements Intake MCP server`,
|
|
157
|
+
isError: true
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
try {
|
|
161
|
+
if (name === "requirement_intake_submit") {
|
|
162
|
+
return await submitIntake(args);
|
|
163
|
+
}
|
|
164
|
+
return await listIntakes(args);
|
|
165
|
+
} catch (error) {
|
|
166
|
+
return {
|
|
167
|
+
content: error instanceof Error ? error.message : String(error),
|
|
168
|
+
isError: true
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
async function submitIntake(args) {
|
|
174
|
+
const request = args["request"];
|
|
175
|
+
if (typeof request !== "string" || request.trim().length === 0) {
|
|
176
|
+
return {
|
|
177
|
+
content: 'requirement_intake_submit requires a non-blank "request" string',
|
|
178
|
+
isError: true
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
const projectId = await resolveProjectId(true);
|
|
182
|
+
const ctx = intakeContext(projectId, actor);
|
|
183
|
+
const result = await service.createIntake(
|
|
184
|
+
{
|
|
185
|
+
projectId,
|
|
186
|
+
originalRequest: request,
|
|
187
|
+
requestedBy: actor,
|
|
188
|
+
...typeof args["title"] === "string" ? { title: args["title"] } : {},
|
|
189
|
+
...typeof args["requestType"] === "string" ? { requestType: args["requestType"] } : {},
|
|
190
|
+
...typeof args["priority"] === "string" ? { priority: args["priority"] } : {},
|
|
191
|
+
...typeof args["idempotencyKey"] === "string" ? { idempotencyKey: args["idempotencyKey"] } : {}
|
|
192
|
+
},
|
|
193
|
+
ctx
|
|
194
|
+
);
|
|
195
|
+
const submitted = await service.submitIntake(result.record.id, ctx);
|
|
196
|
+
return {
|
|
197
|
+
content: {
|
|
198
|
+
intakeId: submitted.record.id,
|
|
199
|
+
title: submitted.record.title,
|
|
200
|
+
requestType: submitted.record.requestType,
|
|
201
|
+
status: submitted.record.status,
|
|
202
|
+
projectId,
|
|
203
|
+
created: result.created,
|
|
204
|
+
idempotent: submitted.idempotent
|
|
205
|
+
},
|
|
206
|
+
isError: false
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
async function listIntakes(args) {
|
|
210
|
+
const projectId = await resolveProjectId(false);
|
|
211
|
+
const ctx = intakeContext(projectId, actor);
|
|
212
|
+
const statuses = filterStatuses(args["statuses"]);
|
|
213
|
+
const records = await service.listIntakes(projectId, ctx, statuses ? { statuses } : void 0);
|
|
214
|
+
return {
|
|
215
|
+
content: {
|
|
216
|
+
projectId,
|
|
217
|
+
count: records.length,
|
|
218
|
+
intakes: records.map((record) => ({
|
|
219
|
+
id: record.id,
|
|
220
|
+
title: record.title,
|
|
221
|
+
requestType: record.requestType,
|
|
222
|
+
status: record.status,
|
|
223
|
+
priority: record.priority,
|
|
224
|
+
updatedAt: record.updatedAt,
|
|
225
|
+
createdAt: record.createdAt
|
|
226
|
+
}))
|
|
227
|
+
},
|
|
228
|
+
isError: false
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
function filterStatuses(value) {
|
|
233
|
+
if (!Array.isArray(value)) return void 0;
|
|
234
|
+
const known = value.filter(
|
|
235
|
+
(item) => INTAKE_STATUSES.includes(String(item))
|
|
236
|
+
);
|
|
237
|
+
return known.length > 0 ? known : void 0;
|
|
238
|
+
}
|
|
239
|
+
function createRequirementIntakeMcpServer(projectRoot, opts = {}) {
|
|
240
|
+
return new MCPServer({
|
|
241
|
+
host: createRequirementIntakeMcpToolHost(projectRoot, opts),
|
|
242
|
+
serverInfo: { name: "wrongstack-requirement-intake-mcp", version: SERVER_INFO.version },
|
|
243
|
+
prompts: [
|
|
244
|
+
{
|
|
245
|
+
name: "file-requirement-intake",
|
|
246
|
+
title: "File a WrongStack requirement intake",
|
|
247
|
+
description: "Record and submit an unstructured software development request as a structured intake record.",
|
|
248
|
+
arguments: [{ name: "request", description: "The exact request text", required: true }],
|
|
249
|
+
template: "Use requirement_intake_submit to record and submit the request {{request}} verbatim. If a matching intake record may already exist, list records first and reuse the idempotency key pattern."
|
|
250
|
+
}
|
|
251
|
+
]
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// src/cli.ts
|
|
256
|
+
function printHelp(stdout) {
|
|
257
|
+
stdout.write(
|
|
258
|
+
[
|
|
259
|
+
`${SERVER_INFO.name} v${SERVER_INFO.version} \u2014 WrongStack Requirements Intake MCP server`,
|
|
260
|
+
"",
|
|
261
|
+
"Usage:",
|
|
262
|
+
` ${SERVER_INFO.name} --project-root <path> [options]`,
|
|
263
|
+
"",
|
|
264
|
+
"Options:",
|
|
265
|
+
" --project-root <path> Project whose requirement intakes should be served (required).",
|
|
266
|
+
" --stdio Use stdio transport (default).",
|
|
267
|
+
" --http Use HTTP transport.",
|
|
268
|
+
" --port <n> HTTP port (default 0 = ephemeral).",
|
|
269
|
+
" --host <h> HTTP bind host (default 127.0.0.1).",
|
|
270
|
+
" --token <t> Bearer token. Required for a non-loopback HTTP bind.",
|
|
271
|
+
" Prefer WRONGSTACK_MCP_TOKEN \u2014 a command line is readable",
|
|
272
|
+
" by other local processes (WS-064).",
|
|
273
|
+
" --actor <id> Actor id recorded as the intake requester.",
|
|
274
|
+
" --writable Expose requirement_intake_submit (filing records).",
|
|
275
|
+
" -h, --help Show this message."
|
|
276
|
+
].join("\n") + "\n"
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
var HTTP_TOKEN_ENV = "WRONGSTACK_MCP_TOKEN";
|
|
280
|
+
function parseArgs(argv, env = process.env) {
|
|
281
|
+
const parsed = {
|
|
282
|
+
projectRoot: "",
|
|
283
|
+
transport: "stdio",
|
|
284
|
+
httpPort: 0,
|
|
285
|
+
httpHost: "127.0.0.1",
|
|
286
|
+
writable: false,
|
|
287
|
+
help: false
|
|
288
|
+
};
|
|
289
|
+
for (let index = 0; index < argv.length; index++) {
|
|
290
|
+
const arg = argv[index];
|
|
291
|
+
switch (arg) {
|
|
292
|
+
case "--project-root":
|
|
293
|
+
parsed.projectRoot = path.resolve(argv[++index] ?? "");
|
|
294
|
+
break;
|
|
295
|
+
case "--stdio":
|
|
296
|
+
parsed.transport = "stdio";
|
|
297
|
+
break;
|
|
298
|
+
case "--http":
|
|
299
|
+
parsed.transport = "http";
|
|
300
|
+
break;
|
|
301
|
+
case "--port":
|
|
302
|
+
parsed.httpPort = Number(argv[++index] ?? "") || 0;
|
|
303
|
+
break;
|
|
304
|
+
case "--host":
|
|
305
|
+
parsed.httpHost = argv[++index] ?? "127.0.0.1";
|
|
306
|
+
break;
|
|
307
|
+
case "--token":
|
|
308
|
+
parsed.httpToken = argv[++index];
|
|
309
|
+
break;
|
|
310
|
+
case "--actor":
|
|
311
|
+
parsed.actor = argv[++index];
|
|
312
|
+
break;
|
|
313
|
+
case "--writable":
|
|
314
|
+
parsed.writable = true;
|
|
315
|
+
break;
|
|
316
|
+
case "-h":
|
|
317
|
+
case "--help":
|
|
318
|
+
parsed.help = true;
|
|
319
|
+
break;
|
|
320
|
+
default:
|
|
321
|
+
break;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
if (parsed.httpToken === void 0) {
|
|
325
|
+
const fromEnv = env[HTTP_TOKEN_ENV]?.trim();
|
|
326
|
+
if (fromEnv) parsed.httpToken = fromEnv;
|
|
327
|
+
} else if (parsed.httpToken.length > 0) {
|
|
328
|
+
console.warn(
|
|
329
|
+
`[mcp] --token puts the auth token in this process's command line, which other local processes can read. Prefer ${HTTP_TOKEN_ENV}.`
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
return parsed;
|
|
333
|
+
}
|
|
334
|
+
async function main(argv = process.argv.slice(2)) {
|
|
335
|
+
const args = parseArgs(argv);
|
|
336
|
+
if (args.help) {
|
|
337
|
+
printHelp(process.stdout);
|
|
338
|
+
return 0;
|
|
339
|
+
}
|
|
340
|
+
if (!args.projectRoot) {
|
|
341
|
+
process.stderr.write(`${SERVER_INFO.name}: --project-root is required
|
|
342
|
+
`);
|
|
343
|
+
printHelp(process.stderr);
|
|
344
|
+
return 2;
|
|
345
|
+
}
|
|
346
|
+
const projectRoot = canonicalProjectRoot(args.projectRoot);
|
|
347
|
+
const server = createRequirementIntakeMcpServer(projectRoot, {
|
|
348
|
+
writable: args.writable,
|
|
349
|
+
...args.actor ? { actor: args.actor } : {}
|
|
350
|
+
});
|
|
351
|
+
const policyText = `writable=${String(args.writable)}`;
|
|
352
|
+
if (args.transport === "http") {
|
|
353
|
+
const handle2 = await serveHttp(server, {
|
|
354
|
+
port: args.httpPort,
|
|
355
|
+
host: args.httpHost,
|
|
356
|
+
...args.httpToken ? { token: args.httpToken } : {},
|
|
357
|
+
logger: { warn: (message) => process.stderr.write(`[requirement-intake-mcp] ${message}
|
|
358
|
+
`) }
|
|
359
|
+
});
|
|
360
|
+
process.stderr.write(
|
|
361
|
+
`${SERVER_INFO.name}: ready at ${handle2.url} \u2014 projectRoot=${projectRoot} transport=http ${policyText}${args.httpToken ? " [token auth]" : ""}
|
|
362
|
+
`
|
|
363
|
+
);
|
|
364
|
+
await new Promise((resolve3) => {
|
|
365
|
+
process.once("SIGINT", resolve3);
|
|
366
|
+
process.once("SIGTERM", resolve3);
|
|
367
|
+
});
|
|
368
|
+
await handle2.close();
|
|
369
|
+
return 0;
|
|
370
|
+
}
|
|
371
|
+
const handle = serveStdio(server);
|
|
372
|
+
process.stderr.write(
|
|
373
|
+
`${SERVER_INFO.name}: ready on stdio \u2014 projectRoot=${projectRoot} transport=stdio ${policyText}
|
|
374
|
+
`
|
|
375
|
+
);
|
|
376
|
+
await handle.done;
|
|
377
|
+
return 0;
|
|
378
|
+
}
|
|
379
|
+
function isMainModule() {
|
|
380
|
+
const entry = process.argv[1];
|
|
381
|
+
if (!entry) return false;
|
|
382
|
+
const self = fileURLToPath2(import.meta.url);
|
|
383
|
+
if (path.resolve(entry) === self) return true;
|
|
384
|
+
try {
|
|
385
|
+
return realpathSync(entry) === realpathSync(self);
|
|
386
|
+
} catch {
|
|
387
|
+
return false;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
if (isMainModule()) {
|
|
391
|
+
main().then(
|
|
392
|
+
(code) => {
|
|
393
|
+
process.exitCode = code;
|
|
394
|
+
},
|
|
395
|
+
(error) => {
|
|
396
|
+
process.stderr.write(`${SERVER_INFO.name}: unexpected error
|
|
397
|
+
`);
|
|
398
|
+
process.stderr.write(error instanceof Error ? error.stack ?? error.message : String(error));
|
|
399
|
+
process.stderr.write("\n");
|
|
400
|
+
process.exitCode = 1;
|
|
401
|
+
}
|
|
402
|
+
);
|
|
403
|
+
}
|
|
404
|
+
export {
|
|
405
|
+
main,
|
|
406
|
+
parseArgs,
|
|
407
|
+
printHelp
|
|
408
|
+
};
|
|
409
|
+
//# sourceMappingURL=cli.js.map
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { createRequirementIntakeMcpServer, createRequirementIntakeMcpToolHost, type RequirementIntakeMcpDependencies, type RequirementIntakeMcpToolHostOptions, } from './adapter.js';
|
|
2
|
+
export { REQUIREMENT_INTAKE_READ_TOOLS, REQUIREMENT_INTAKE_WRITE_TOOLS, type RequirementIntakeMcpPolicyOptions, type RequirementIntakeMcpToolName, type RequirementIntakeMcpToolPolicy, selectRequirementIntakeTools, } from './policy.js';
|
|
3
|
+
export { SERVER_INFO } from './version.js';
|
|
4
|
+
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
// src/adapter.ts
|
|
2
|
+
import {
|
|
3
|
+
ensureProjectIdentity,
|
|
4
|
+
readProjectIdentity,
|
|
5
|
+
resolveWstackPaths
|
|
6
|
+
} from "@wrongstack/core/utils";
|
|
7
|
+
import {
|
|
8
|
+
MCPServer
|
|
9
|
+
} from "@wrongstack/mcp";
|
|
10
|
+
import {
|
|
11
|
+
AllowAllIntakeAuthorizer,
|
|
12
|
+
INTAKE_PRIORITIES,
|
|
13
|
+
INTAKE_STATUSES,
|
|
14
|
+
RequirementIntakeService,
|
|
15
|
+
RequirementIntakeStore
|
|
16
|
+
} from "@wrongstack/requirement-intake";
|
|
17
|
+
|
|
18
|
+
// src/policy.ts
|
|
19
|
+
var REQUIREMENT_INTAKE_READ_TOOLS = ["requirement_intake_list"];
|
|
20
|
+
var REQUIREMENT_INTAKE_WRITE_TOOLS = ["requirement_intake_submit"];
|
|
21
|
+
function selectRequirementIntakeTools(opts = {}) {
|
|
22
|
+
const tools = [{ name: "requirement_intake_list" }];
|
|
23
|
+
if (opts.writable === true) {
|
|
24
|
+
tools.push({ name: "requirement_intake_submit" });
|
|
25
|
+
}
|
|
26
|
+
return tools;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// src/version.ts
|
|
30
|
+
import { readFileSync } from "node:fs";
|
|
31
|
+
import { dirname, resolve } from "node:path";
|
|
32
|
+
import { fileURLToPath } from "node:url";
|
|
33
|
+
var here = dirname(fileURLToPath(import.meta.url));
|
|
34
|
+
var packagePath = resolve(here, "..", "package.json");
|
|
35
|
+
var cached;
|
|
36
|
+
function readServerInfo() {
|
|
37
|
+
if (cached) return cached;
|
|
38
|
+
try {
|
|
39
|
+
const pkg = JSON.parse(readFileSync(packagePath, "utf8"));
|
|
40
|
+
cached = {
|
|
41
|
+
name: pkg.name ?? "@wrongstack/requirement-intake-mcp",
|
|
42
|
+
version: pkg.version ?? "0.0.0"
|
|
43
|
+
};
|
|
44
|
+
} catch {
|
|
45
|
+
cached = { name: "@wrongstack/requirement-intake-mcp", version: "0.0.0" };
|
|
46
|
+
}
|
|
47
|
+
return cached;
|
|
48
|
+
}
|
|
49
|
+
var SERVER_INFO = readServerInfo();
|
|
50
|
+
|
|
51
|
+
// src/adapter.ts
|
|
52
|
+
var SUBMIT_SCHEMA = {
|
|
53
|
+
type: "object",
|
|
54
|
+
properties: {
|
|
55
|
+
request: {
|
|
56
|
+
type: "string",
|
|
57
|
+
description: "The exact software development request to record \u2014 a feature, bug fix, refactor, UI/API/infra change, migration, documentation, etc. Preserved verbatim."
|
|
58
|
+
},
|
|
59
|
+
title: {
|
|
60
|
+
type: "string",
|
|
61
|
+
description: "Optional short title. Defaults to a deterministic title from the request."
|
|
62
|
+
},
|
|
63
|
+
requestType: {
|
|
64
|
+
type: "string",
|
|
65
|
+
enum: [
|
|
66
|
+
"feature",
|
|
67
|
+
"bug_fix",
|
|
68
|
+
"refactor",
|
|
69
|
+
"performance",
|
|
70
|
+
"security",
|
|
71
|
+
"ui_change",
|
|
72
|
+
"api_change",
|
|
73
|
+
"infrastructure",
|
|
74
|
+
"migration",
|
|
75
|
+
"testing",
|
|
76
|
+
"documentation",
|
|
77
|
+
"maintenance",
|
|
78
|
+
"other",
|
|
79
|
+
"unspecified"
|
|
80
|
+
],
|
|
81
|
+
description: "Request type hint. Unknown values normalize to other/unspecified."
|
|
82
|
+
},
|
|
83
|
+
priority: {
|
|
84
|
+
type: "string",
|
|
85
|
+
enum: [...INTAKE_PRIORITIES],
|
|
86
|
+
description: "Desired priority."
|
|
87
|
+
},
|
|
88
|
+
idempotencyKey: {
|
|
89
|
+
type: "string",
|
|
90
|
+
description: "Optional key making create idempotent \u2014 retries return the existing record."
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
required: ["request"],
|
|
94
|
+
additionalProperties: false
|
|
95
|
+
};
|
|
96
|
+
var LIST_SCHEMA = {
|
|
97
|
+
type: "object",
|
|
98
|
+
properties: {
|
|
99
|
+
statuses: {
|
|
100
|
+
type: "array",
|
|
101
|
+
items: { type: "string", enum: [...INTAKE_STATUSES] },
|
|
102
|
+
description: "Optional status filter (draft, collecting_information, submitted, cancelled, archived)."
|
|
103
|
+
}
|
|
104
|
+
},
|
|
105
|
+
additionalProperties: false
|
|
106
|
+
};
|
|
107
|
+
var TOOL_DESCRIPTIONS = {
|
|
108
|
+
requirement_intake_list: "List requirement intake records for the project, newest first, optionally filtered by status.",
|
|
109
|
+
requirement_intake_submit: "File and submit a requirement intake record from the given request text. Requires server --writable."
|
|
110
|
+
};
|
|
111
|
+
function toolDescriptor(name) {
|
|
112
|
+
return {
|
|
113
|
+
name,
|
|
114
|
+
description: TOOL_DESCRIPTIONS[name],
|
|
115
|
+
inputSchema: name === "requirement_intake_submit" ? SUBMIT_SCHEMA : LIST_SCHEMA
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
function intakeContext(projectId, actor) {
|
|
119
|
+
return { id: actor, type: "automation", projectId };
|
|
120
|
+
}
|
|
121
|
+
async function defaultResolveProjectId(projectRoot, createIfMissing) {
|
|
122
|
+
const existing = await readProjectIdentity(projectRoot);
|
|
123
|
+
if (existing) return existing.projectId;
|
|
124
|
+
if (!createIfMissing) {
|
|
125
|
+
throw new Error(
|
|
126
|
+
"No WrongStack project identity found \u2014 run `wstack init` (or file an intake first) to create it"
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
return (await ensureProjectIdentity(projectRoot)).identity.projectId;
|
|
130
|
+
}
|
|
131
|
+
function createRequirementIntakeMcpToolHost(projectRoot, opts = {}) {
|
|
132
|
+
const policy = selectRequirementIntakeTools(opts);
|
|
133
|
+
const allowed = new Set(policy.map((entry) => entry.name));
|
|
134
|
+
const actor = opts.actor?.trim() || "external-intake-mcp";
|
|
135
|
+
const service = opts.dependencies?.service ?? new RequirementIntakeService({
|
|
136
|
+
store: new RequirementIntakeStore({
|
|
137
|
+
baseDir: resolveWstackPaths({ projectRoot }).projectRequirementIntakes
|
|
138
|
+
}),
|
|
139
|
+
authorizer: new AllowAllIntakeAuthorizer()
|
|
140
|
+
});
|
|
141
|
+
const resolveProjectId = opts.dependencies?.resolveProjectId ?? ((createIfMissing) => defaultResolveProjectId(projectRoot, createIfMissing));
|
|
142
|
+
return {
|
|
143
|
+
listTools() {
|
|
144
|
+
return policy.map((entry) => toolDescriptor(entry.name));
|
|
145
|
+
},
|
|
146
|
+
async callTool(name, args) {
|
|
147
|
+
if (!allowed.has(name)) {
|
|
148
|
+
return {
|
|
149
|
+
content: `Tool "${name}" is not exposed by this Requirements Intake MCP server`,
|
|
150
|
+
isError: true
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
try {
|
|
154
|
+
if (name === "requirement_intake_submit") {
|
|
155
|
+
return await submitIntake(args);
|
|
156
|
+
}
|
|
157
|
+
return await listIntakes(args);
|
|
158
|
+
} catch (error) {
|
|
159
|
+
return {
|
|
160
|
+
content: error instanceof Error ? error.message : String(error),
|
|
161
|
+
isError: true
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
async function submitIntake(args) {
|
|
167
|
+
const request = args["request"];
|
|
168
|
+
if (typeof request !== "string" || request.trim().length === 0) {
|
|
169
|
+
return {
|
|
170
|
+
content: 'requirement_intake_submit requires a non-blank "request" string',
|
|
171
|
+
isError: true
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
const projectId = await resolveProjectId(true);
|
|
175
|
+
const ctx = intakeContext(projectId, actor);
|
|
176
|
+
const result = await service.createIntake(
|
|
177
|
+
{
|
|
178
|
+
projectId,
|
|
179
|
+
originalRequest: request,
|
|
180
|
+
requestedBy: actor,
|
|
181
|
+
...typeof args["title"] === "string" ? { title: args["title"] } : {},
|
|
182
|
+
...typeof args["requestType"] === "string" ? { requestType: args["requestType"] } : {},
|
|
183
|
+
...typeof args["priority"] === "string" ? { priority: args["priority"] } : {},
|
|
184
|
+
...typeof args["idempotencyKey"] === "string" ? { idempotencyKey: args["idempotencyKey"] } : {}
|
|
185
|
+
},
|
|
186
|
+
ctx
|
|
187
|
+
);
|
|
188
|
+
const submitted = await service.submitIntake(result.record.id, ctx);
|
|
189
|
+
return {
|
|
190
|
+
content: {
|
|
191
|
+
intakeId: submitted.record.id,
|
|
192
|
+
title: submitted.record.title,
|
|
193
|
+
requestType: submitted.record.requestType,
|
|
194
|
+
status: submitted.record.status,
|
|
195
|
+
projectId,
|
|
196
|
+
created: result.created,
|
|
197
|
+
idempotent: submitted.idempotent
|
|
198
|
+
},
|
|
199
|
+
isError: false
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
async function listIntakes(args) {
|
|
203
|
+
const projectId = await resolveProjectId(false);
|
|
204
|
+
const ctx = intakeContext(projectId, actor);
|
|
205
|
+
const statuses = filterStatuses(args["statuses"]);
|
|
206
|
+
const records = await service.listIntakes(projectId, ctx, statuses ? { statuses } : void 0);
|
|
207
|
+
return {
|
|
208
|
+
content: {
|
|
209
|
+
projectId,
|
|
210
|
+
count: records.length,
|
|
211
|
+
intakes: records.map((record) => ({
|
|
212
|
+
id: record.id,
|
|
213
|
+
title: record.title,
|
|
214
|
+
requestType: record.requestType,
|
|
215
|
+
status: record.status,
|
|
216
|
+
priority: record.priority,
|
|
217
|
+
updatedAt: record.updatedAt,
|
|
218
|
+
createdAt: record.createdAt
|
|
219
|
+
}))
|
|
220
|
+
},
|
|
221
|
+
isError: false
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
function filterStatuses(value) {
|
|
226
|
+
if (!Array.isArray(value)) return void 0;
|
|
227
|
+
const known = value.filter(
|
|
228
|
+
(item) => INTAKE_STATUSES.includes(String(item))
|
|
229
|
+
);
|
|
230
|
+
return known.length > 0 ? known : void 0;
|
|
231
|
+
}
|
|
232
|
+
function createRequirementIntakeMcpServer(projectRoot, opts = {}) {
|
|
233
|
+
return new MCPServer({
|
|
234
|
+
host: createRequirementIntakeMcpToolHost(projectRoot, opts),
|
|
235
|
+
serverInfo: { name: "wrongstack-requirement-intake-mcp", version: SERVER_INFO.version },
|
|
236
|
+
prompts: [
|
|
237
|
+
{
|
|
238
|
+
name: "file-requirement-intake",
|
|
239
|
+
title: "File a WrongStack requirement intake",
|
|
240
|
+
description: "Record and submit an unstructured software development request as a structured intake record.",
|
|
241
|
+
arguments: [{ name: "request", description: "The exact request text", required: true }],
|
|
242
|
+
template: "Use requirement_intake_submit to record and submit the request {{request}} verbatim. If a matching intake record may already exist, list records first and reuse the idempotency key pattern."
|
|
243
|
+
}
|
|
244
|
+
]
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
export {
|
|
248
|
+
REQUIREMENT_INTAKE_READ_TOOLS,
|
|
249
|
+
REQUIREMENT_INTAKE_WRITE_TOOLS,
|
|
250
|
+
SERVER_INFO,
|
|
251
|
+
createRequirementIntakeMcpServer,
|
|
252
|
+
createRequirementIntakeMcpToolHost,
|
|
253
|
+
selectRequirementIntakeTools
|
|
254
|
+
};
|
|
255
|
+
//# sourceMappingURL=index.js.map
|
package/dist/policy.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Requirements Intake MCP — capability tiers.
|
|
3
|
+
*
|
|
4
|
+
* Mirrors the Kanban MCP policy shape: read-only tooling is always exposed;
|
|
5
|
+
* mutation (filing + submitting intake records) requires the writable tier.
|
|
6
|
+
*/
|
|
7
|
+
export declare const REQUIREMENT_INTAKE_READ_TOOLS: readonly ['requirement_intake_list'];
|
|
8
|
+
export declare const REQUIREMENT_INTAKE_WRITE_TOOLS: readonly ['requirement_intake_submit'];
|
|
9
|
+
export type RequirementIntakeMcpToolName = (typeof REQUIREMENT_INTAKE_READ_TOOLS)[number] | (typeof REQUIREMENT_INTAKE_WRITE_TOOLS)[number];
|
|
10
|
+
export interface RequirementIntakeMcpPolicyOptions {
|
|
11
|
+
/** Expose the writable `requirement_intake_submit` tool. Default: read-only. */
|
|
12
|
+
writable?: boolean;
|
|
13
|
+
}
|
|
14
|
+
export interface RequirementIntakeMcpToolPolicy {
|
|
15
|
+
name: RequirementIntakeMcpToolName;
|
|
16
|
+
}
|
|
17
|
+
export declare function selectRequirementIntakeTools(opts?: RequirementIntakeMcpPolicyOptions): RequirementIntakeMcpToolPolicy[];
|
|
18
|
+
//# sourceMappingURL=policy.d.ts.map
|
package/package.json
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@wrongstack/requirement-intake-mcp",
|
|
3
|
+
"version": "0.299.0",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"description": "WrongStack Requirements Intake as a project-scoped MCP server: list intake records (read tier) and file+submit intake records (writable tier).",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/WrongStack/WrongStack.git",
|
|
9
|
+
"directory": "packages/requirement-intake-mcp"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://github.com/WrongStack/WrongStack#readme",
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/WrongStack/WrongStack/issues"
|
|
14
|
+
},
|
|
15
|
+
"author": "ECOSTACK TECHNOLOGY OÜ",
|
|
16
|
+
"type": "module",
|
|
17
|
+
"main": "./dist/index.js",
|
|
18
|
+
"types": "./dist/index.d.ts",
|
|
19
|
+
"bin": {
|
|
20
|
+
"wstack-requirement-intake-mcp": "./dist/cli.js"
|
|
21
|
+
},
|
|
22
|
+
"sideEffects": false,
|
|
23
|
+
"exports": {
|
|
24
|
+
".": {
|
|
25
|
+
"types": "./dist/index.d.ts",
|
|
26
|
+
"import": "./dist/index.js"
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
"files": [
|
|
30
|
+
"dist",
|
|
31
|
+
"!dist/**/*.map",
|
|
32
|
+
"README.md"
|
|
33
|
+
],
|
|
34
|
+
"dependencies": {
|
|
35
|
+
"@wrongstack/mcp": "0.299.0",
|
|
36
|
+
"@wrongstack/core": "0.299.0",
|
|
37
|
+
"@wrongstack/requirement-intake": "0.299.0"
|
|
38
|
+
},
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"@types/node": "^26.1.2",
|
|
41
|
+
"typescript": "^7.0.2",
|
|
42
|
+
"vitest": "^4.1.10"
|
|
43
|
+
},
|
|
44
|
+
"publishConfig": {
|
|
45
|
+
"access": "public"
|
|
46
|
+
},
|
|
47
|
+
"scripts": {
|
|
48
|
+
"build": "node ../../scripts/build-package.mjs",
|
|
49
|
+
"typecheck": "tsc --noEmit -p tsconfig.test.json",
|
|
50
|
+
"test": "echo \"Run @wrongstack/requirement-intake-mcp tests from the workspace root: pnpm exec vitest run packages/requirement-intake-mcp/tests\"",
|
|
51
|
+
"clean": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\""
|
|
52
|
+
}
|
|
53
|
+
}
|