@ebitex/content-mcp 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/README.md +102 -0
  2. package/dist/client.d.ts +34 -0
  3. package/dist/client.d.ts.map +1 -0
  4. package/dist/client.js +63 -0
  5. package/dist/client.js.map +1 -0
  6. package/dist/config.d.ts +42 -0
  7. package/dist/config.d.ts.map +1 -0
  8. package/dist/config.js +62 -0
  9. package/dist/config.js.map +1 -0
  10. package/dist/errors.d.ts +43 -0
  11. package/dist/errors.d.ts.map +1 -0
  12. package/dist/errors.js +82 -0
  13. package/dist/errors.js.map +1 -0
  14. package/dist/index.d.ts +14 -0
  15. package/dist/index.d.ts.map +1 -0
  16. package/dist/index.js +14 -0
  17. package/dist/index.js.map +1 -0
  18. package/dist/main.d.ts +3 -0
  19. package/dist/main.d.ts.map +1 -0
  20. package/dist/main.js +35 -0
  21. package/dist/main.js.map +1 -0
  22. package/dist/resources/helpCorpus.generated.d.ts +7 -0
  23. package/dist/resources/helpCorpus.generated.d.ts.map +1 -0
  24. package/dist/resources/helpCorpus.generated.js +112 -0
  25. package/dist/resources/helpCorpus.generated.js.map +1 -0
  26. package/dist/resources/register.d.ts +8 -0
  27. package/dist/resources/register.d.ts.map +1 -0
  28. package/dist/resources/register.js +73 -0
  29. package/dist/resources/register.js.map +1 -0
  30. package/dist/routes.d.ts +68 -0
  31. package/dist/routes.d.ts.map +1 -0
  32. package/dist/routes.js +173 -0
  33. package/dist/routes.js.map +1 -0
  34. package/dist/server.d.ts +32 -0
  35. package/dist/server.d.ts.map +1 -0
  36. package/dist/server.js +65 -0
  37. package/dist/server.js.map +1 -0
  38. package/dist/session.d.ts +45 -0
  39. package/dist/session.d.ts.map +1 -0
  40. package/dist/session.js +71 -0
  41. package/dist/session.js.map +1 -0
  42. package/dist/tools/register.d.ts +26 -0
  43. package/dist/tools/register.d.ts.map +1 -0
  44. package/dist/tools/register.js +212 -0
  45. package/dist/tools/register.js.map +1 -0
  46. package/dist/tools/result.d.ts +28 -0
  47. package/dist/tools/result.d.ts.map +1 -0
  48. package/dist/tools/result.js +26 -0
  49. package/dist/tools/result.js.map +1 -0
  50. package/package.json +39 -0
package/README.md ADDED
@@ -0,0 +1,102 @@
1
+ # @ebitex/content-mcp
2
+
3
+ An [MCP](https://modelcontextprotocol.io) server for **ebitex Content**. Point an AI agent at it and
4
+ it can build a content model end to end — Contracts and their fields, Templates, Components, the
5
+ Experience tree — and publish it.
6
+
7
+ It runs locally, over `stdio`, authenticated by a Content management key you mint yourself. Nothing
8
+ is hosted by ebitex, so your write-capable key never leaves your machine.
9
+
10
+ ## Configure it
11
+
12
+ ```json
13
+ {
14
+ "mcpServers": {
15
+ "ebitex-content": {
16
+ "command": "npx",
17
+ "args": ["-y", "@ebitex/content-mcp"],
18
+ "env": { "EBITEX_CONTENT_MANAGEMENT_KEY": "frm_live_..." }
19
+ }
20
+ }
21
+ }
22
+ ```
23
+
24
+ | Variable | |
25
+ |---|---|
26
+ | `EBITEX_CONTENT_MANAGEMENT_KEY` | **Required.** Mint one in Composer under Settings → Management keys |
27
+ | `EBITEX_CONTENT_API_BASE` | Defaults to `https://api.ebitex.io` |
28
+ | `EBITEX_CONTENT_MCP_READ_ONLY` | Set to `1` to hide every write tool regardless of the key's scopes |
29
+ | `EBITEX_CONTENT_TARGET_MANAGEMENT_KEY` | A second key, for `content_transfer` to import into |
30
+
31
+ ## Mint the key deliberately
32
+
33
+ A management key is bound to **one organization and one authoring environment**, and acts as **one
34
+ role**. That binding is the real safety boundary here, and it is the one this server cannot supply
35
+ for you:
36
+
37
+ - **Bind it to a non-production authoring environment.** This server has no way to tell which of
38
+ your environments is production — that is your own topology, and a management key deliberately
39
+ cannot see it. A heuristic would be worse than nothing, because it would be believed.
40
+ - **Leave the authoring scope off unless the agent needs to write.** Without it, the write tools are
41
+ not merely refused; they are not offered at all.
42
+ - **Give it a role with the access it needs and no more.** Branch access rules and workflow gating
43
+ apply to a key exactly as they do to a person.
44
+
45
+ On startup the server prints, to stderr, which organization and environment the key reached and
46
+ which role it acts as. Read it once; if it is not what you expected, stop.
47
+
48
+ ## The tools
49
+
50
+ Eight, shaped around what an agent does rather than around REST routes.
51
+
52
+ | | |
53
+ |---|---|
54
+ | `content_describe` | What can exist here: every field type with its **own settings schema**, plus this environment's Contracts and Templates |
55
+ | `content_find` | Locate entities by kind, search or external id |
56
+ | `content_get` | Read one entity in full |
57
+ | `content_write` | Create or update one entity |
58
+ | `content_delete` | Delete one, behind an explicit confirmation |
59
+ | `content_publish` | Plan, read the blockers, then publish |
60
+ | `content_operation_status` | Poll a job |
61
+ | `content_transfer` | Move a whole model between environments, atomically |
62
+
63
+ Which of them appear depends on your key's scopes: a read-scoped key gets a server with no write
64
+ tools at all, rather than tools that always fail.
65
+
66
+ ## Why it cannot drift
67
+
68
+ The server encodes **no knowledge of the content model**. It does not know what field types exist,
69
+ what a Contract may contain, or what makes a document valid. All three are answered at runtime:
70
+ `content_describe` returns each field type's own declared schema, and every write is validated by the
71
+ server that stores it. There is nothing here to fall out of step, because nothing is duplicated.
72
+
73
+ That also shapes how errors work. A refusal is relayed **whole**, with its field paths intact and no
74
+ summarizing, because for an agent the error is the control loop: try, read the error, fix the one
75
+ field it names, call again.
76
+
77
+ ## Resources
78
+
79
+ Alongside the tools, the server offers the Content documentation as MCP resources — start with
80
+ `ebitex-content://help/agent-authoring`, which explains what Contracts, Templates, Presentations,
81
+ Adapters and the Experience tree are for, and the order to create them in.
82
+
83
+ ## Embedding it
84
+
85
+ ```ts
86
+ import { buildServer, readConfig } from '@ebitex/content-mcp'
87
+
88
+ const { server, banner } = await buildServer({ config: readConfig() })
89
+ console.error(banner) // never stdout — on stdio, that stream is the protocol
90
+ ```
91
+
92
+ ## A note on `zod`
93
+
94
+ This package pins `zod` to `^3.25.76` rather than `^4`, even though `@modelcontextprotocol/sdk`
95
+ accepts either. The reason is type identity, not capability: the SDK's schema types are written
96
+ against whichever `zod` resolves for *it*, and a second copy at a different major produces a type
97
+ mismatch on every tool registration. Pinning to the version the SDK resolves keeps one `zod` in the
98
+ tree. This matters at build time only; at runtime the SDK handles both.
99
+
100
+ ## Licence
101
+
102
+ UNLICENSED — © ebitex software LLC.
@@ -0,0 +1,34 @@
1
+ /**
2
+ * The HTTP client for `/content/management/v1`.
3
+ *
4
+ * Deliberately **one method per verb**, not one per endpoint. The endpoints come from
5
+ * `routes.ts`'s table, which is the only place in this package that knows a route exists — so a
6
+ * family admitted by a later spec is a row there rather than a method here, and there is no second
7
+ * list to keep in step with the first.
8
+ *
9
+ * It is also the only module that holds the key, and the only one that calls `fetch`. Both are
10
+ * worth keeping true: a test drives the whole server by substituting one function.
11
+ */
12
+ export declare const ROUTE_PREFIX = "/content/management/v1";
13
+ export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
14
+ export type ContentClient = {
15
+ /** Origin only, for messages that need to say where writes are landing. */
16
+ readonly baseUrl: string;
17
+ request(method: HttpMethod, path: string, body?: unknown): Promise<unknown>;
18
+ };
19
+ export type ClientOptions = {
20
+ baseUrl: string;
21
+ key: string;
22
+ /** Substituted by tests. Defaults to the global `fetch`. */
23
+ fetchImpl?: typeof fetch;
24
+ };
25
+ export declare function createClient(options: ClientOptions): ContentClient;
26
+ /**
27
+ * Builds a query string from values that may be absent, skipping the absent ones.
28
+ *
29
+ * Kept here rather than inlined because the API rejects unknown *properties* on a body but is
30
+ * ordinary about query parameters — the asymmetry is easy to forget, and sending `limit=undefined`
31
+ * as a literal string is the kind of thing that reads as an API bug for an afternoon.
32
+ */
33
+ export declare function query(params: Record<string, string | number | boolean | undefined | null>): string;
34
+ //# sourceMappingURL=client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;GAUG;AAEH,eAAO,MAAM,YAAY,2BAA2B,CAAA;AAEpD,MAAM,MAAM,UAAU,GAAG,KAAK,GAAG,MAAM,GAAG,KAAK,GAAG,OAAO,GAAG,QAAQ,CAAA;AAEpE,MAAM,MAAM,aAAa,GAAG;IAC1B,2EAA2E;IAC3E,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,OAAO,CAAC,MAAM,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAA;CAC5E,CAAA;AAED,MAAM,MAAM,aAAa,GAAG;IAC1B,OAAO,EAAE,MAAM,CAAA;IACf,GAAG,EAAE,MAAM,CAAA;IACX,4DAA4D;IAC5D,SAAS,CAAC,EAAE,OAAO,KAAK,CAAA;CACzB,CAAA;AAED,wBAAgB,YAAY,CAAC,OAAO,EAAE,aAAa,GAAG,aAAa,CAgClE;AAED;;;;;;GAMG;AACH,wBAAgB,KAAK,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,SAAS,GAAG,IAAI,CAAC,GAAG,MAAM,CASlG"}
package/dist/client.js ADDED
@@ -0,0 +1,63 @@
1
+ import { toContentApiError } from './errors.js';
2
+ /**
3
+ * The HTTP client for `/content/management/v1`.
4
+ *
5
+ * Deliberately **one method per verb**, not one per endpoint. The endpoints come from
6
+ * `routes.ts`'s table, which is the only place in this package that knows a route exists — so a
7
+ * family admitted by a later spec is a row there rather than a method here, and there is no second
8
+ * list to keep in step with the first.
9
+ *
10
+ * It is also the only module that holds the key, and the only one that calls `fetch`. Both are
11
+ * worth keeping true: a test drives the whole server by substituting one function.
12
+ */
13
+ export const ROUTE_PREFIX = '/content/management/v1';
14
+ export function createClient(options) {
15
+ const doFetch = options.fetchImpl ?? globalThis.fetch;
16
+ return {
17
+ baseUrl: options.baseUrl,
18
+ async request(method, path, body) {
19
+ const response = await doFetch(`${options.baseUrl}${ROUTE_PREFIX}${path}`, {
20
+ method,
21
+ headers: {
22
+ Authorization: `Bearer ${options.key}`,
23
+ Accept: 'application/json',
24
+ ...(body === undefined ? {} : { 'Content-Type': 'application/json' }),
25
+ },
26
+ body: body === undefined ? undefined : JSON.stringify(body),
27
+ });
28
+ if (!response.ok)
29
+ throw await toContentApiError(response);
30
+ // 202 (both publish phases) and 204 (every delete) carry no body. Returning `null` rather
31
+ // than throwing on an empty parse keeps the caller from having to know which is which.
32
+ if (response.status === 204)
33
+ return null;
34
+ const text = await response.text();
35
+ if (!text)
36
+ return null;
37
+ try {
38
+ return JSON.parse(text);
39
+ }
40
+ catch {
41
+ return text;
42
+ }
43
+ },
44
+ };
45
+ }
46
+ /**
47
+ * Builds a query string from values that may be absent, skipping the absent ones.
48
+ *
49
+ * Kept here rather than inlined because the API rejects unknown *properties* on a body but is
50
+ * ordinary about query parameters — the asymmetry is easy to forget, and sending `limit=undefined`
51
+ * as a literal string is the kind of thing that reads as an API bug for an afternoon.
52
+ */
53
+ export function query(params) {
54
+ const search = new URLSearchParams();
55
+ for (const [name, value] of Object.entries(params)) {
56
+ if (value === undefined || value === null || value === '')
57
+ continue;
58
+ search.set(name, String(value));
59
+ }
60
+ const rendered = search.toString();
61
+ return rendered ? `?${rendered}` : '';
62
+ }
63
+ //# sourceMappingURL=client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAA;AAE/C;;;;;;;;;;GAUG;AAEH,MAAM,CAAC,MAAM,YAAY,GAAG,wBAAwB,CAAA;AAiBpD,MAAM,UAAU,YAAY,CAAC,OAAsB;IACjD,MAAM,OAAO,GAAG,OAAO,CAAC,SAAS,IAAI,UAAU,CAAC,KAAK,CAAA;IAErD,OAAO;QACL,OAAO,EAAE,OAAO,CAAC,OAAO;QACxB,KAAK,CAAC,OAAO,CAAC,MAAM,EAAE,IAAI,EAAE,IAAI;YAC9B,MAAM,QAAQ,GAAG,MAAM,OAAO,CAAC,GAAG,OAAO,CAAC,OAAO,GAAG,YAAY,GAAG,IAAI,EAAE,EAAE;gBACzE,MAAM;gBACN,OAAO,EAAE;oBACP,aAAa,EAAE,UAAU,OAAO,CAAC,GAAG,EAAE;oBACtC,MAAM,EAAE,kBAAkB;oBAC1B,GAAG,CAAC,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,cAAc,EAAE,kBAAkB,EAAE,CAAC;iBACtE;gBACD,IAAI,EAAE,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC;aAC5D,CAAC,CAAA;YAEF,IAAI,CAAC,QAAQ,CAAC,EAAE;gBAAE,MAAM,MAAM,iBAAiB,CAAC,QAAQ,CAAC,CAAA;YAEzD,0FAA0F;YAC1F,uFAAuF;YACvF,IAAI,QAAQ,CAAC,MAAM,KAAK,GAAG;gBAAE,OAAO,IAAI,CAAA;YAExC,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAA;YAClC,IAAI,CAAC,IAAI;gBAAE,OAAO,IAAI,CAAA;YAEtB,IAAI,CAAC;gBACH,OAAO,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAA;YACzB,CAAC;YAAC,MAAM,CAAC;gBACP,OAAO,IAAI,CAAA;YACb,CAAC;QACH,CAAC;KACF,CAAA;AACH,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,KAAK,CAAC,MAAoE;IACxF,MAAM,MAAM,GAAG,IAAI,eAAe,EAAE,CAAA;IACpC,KAAK,MAAM,CAAC,IAAI,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;QACnD,IAAI,KAAK,KAAK,SAAS,IAAI,KAAK,KAAK,IAAI,IAAI,KAAK,KAAK,EAAE;YAAE,SAAQ;QACnE,MAAM,CAAC,GAAG,CAAC,IAAI,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC,CAAA;IACjC,CAAC;IAED,MAAM,QAAQ,GAAG,MAAM,CAAC,QAAQ,EAAE,CAAA;IAClC,OAAO,QAAQ,CAAC,CAAC,CAAC,IAAI,QAAQ,EAAE,CAAC,CAAC,CAAC,EAAE,CAAA;AACvC,CAAC"}
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Everything this server reads from its environment, in one place.
3
+ *
4
+ * An MCP server is launched by a host process with a config block, not by a person at a prompt, so
5
+ * a misconfiguration has no interactive moment in which to be noticed. That makes the failure mode
6
+ * the thing to design for: a missing key must stop the server at startup naming the variable, never
7
+ * surface later as a `401` on whatever tool the agent happened to call first. An agent reading a
8
+ * `401` will reasonably conclude the credential is wrong rather than absent, and will retry.
9
+ */
10
+ /** The one required variable. Minted in Composer under Settings > Management keys. */
11
+ export declare const KEY_VAR = "EBITEX_CONTENT_MANAGEMENT_KEY";
12
+ /** Where the API lives. Only ever changed for a local AppHost or a self-hosted deployment. */
13
+ export declare const BASE_VAR = "EBITEX_CONTENT_API_BASE";
14
+ /**
15
+ * An operator's declaration that this session must not write, independent of what the key could do.
16
+ *
17
+ * It exists because a key's scopes are not readable: the credential carries no scope list, and no
18
+ * endpoint reports one (see DEV_PLAN's Open Questions). So "hide the write tools" cannot be derived
19
+ * from the key, and the honest alternatives are to advertise writes always — which sends an agent
20
+ * down paths that will `403` — or to let the operator say. This is the operator saying.
21
+ */
22
+ export declare const READ_ONLY_VAR = "EBITEX_CONTENT_MCP_READ_ONLY";
23
+ /** A second key, bound to the environment `content_transfer` imports *into*. */
24
+ export declare const TARGET_KEY_VAR = "EBITEX_CONTENT_TARGET_MANAGEMENT_KEY";
25
+ export declare const DEFAULT_BASE = "https://api.ebitex.io";
26
+ export type ServerConfig = {
27
+ /** Origin only — `/content/management/v1` is appended by the client, never by a caller. */
28
+ baseUrl: string;
29
+ key: string;
30
+ /** Present only when {@link TARGET_KEY_VAR} is set; `content_transfer` is otherwise export-only. */
31
+ targetKey: string | null;
32
+ readOnly: boolean;
33
+ };
34
+ export declare class ConfigError extends Error {
35
+ }
36
+ /**
37
+ * Reads the configuration, or throws {@link ConfigError} with a message written for the person
38
+ * editing an MCP client's JSON — it names the variable and where to get its value, because that is
39
+ * the only thing they can act on.
40
+ */
41
+ export declare function readConfig(env?: Record<string, string | undefined>): ServerConfig;
42
+ //# sourceMappingURL=config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,sFAAsF;AACtF,eAAO,MAAM,OAAO,kCAAkC,CAAA;AAEtD,8FAA8F;AAC9F,eAAO,MAAM,QAAQ,4BAA4B,CAAA;AAEjD;;;;;;;GAOG;AACH,eAAO,MAAM,aAAa,iCAAiC,CAAA;AAE3D,gFAAgF;AAChF,eAAO,MAAM,cAAc,yCAAyC,CAAA;AAEpE,eAAO,MAAM,YAAY,0BAA0B,CAAA;AAEnD,MAAM,MAAM,YAAY,GAAG;IACzB,2FAA2F;IAC3F,OAAO,EAAE,MAAM,CAAA;IACf,GAAG,EAAE,MAAM,CAAA;IACX,oGAAoG;IACpG,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,QAAQ,EAAE,OAAO,CAAA;CAClB,CAAA;AAED,qBAAa,WAAY,SAAQ,KAAK;CAAG;AAEzC;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,GAAG,GAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAe,GAAG,YAAY,CAyB9F"}
package/dist/config.js ADDED
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Everything this server reads from its environment, in one place.
3
+ *
4
+ * An MCP server is launched by a host process with a config block, not by a person at a prompt, so
5
+ * a misconfiguration has no interactive moment in which to be noticed. That makes the failure mode
6
+ * the thing to design for: a missing key must stop the server at startup naming the variable, never
7
+ * surface later as a `401` on whatever tool the agent happened to call first. An agent reading a
8
+ * `401` will reasonably conclude the credential is wrong rather than absent, and will retry.
9
+ */
10
+ /** The one required variable. Minted in Composer under Settings > Management keys. */
11
+ export const KEY_VAR = 'EBITEX_CONTENT_MANAGEMENT_KEY';
12
+ /** Where the API lives. Only ever changed for a local AppHost or a self-hosted deployment. */
13
+ export const BASE_VAR = 'EBITEX_CONTENT_API_BASE';
14
+ /**
15
+ * An operator's declaration that this session must not write, independent of what the key could do.
16
+ *
17
+ * It exists because a key's scopes are not readable: the credential carries no scope list, and no
18
+ * endpoint reports one (see DEV_PLAN's Open Questions). So "hide the write tools" cannot be derived
19
+ * from the key, and the honest alternatives are to advertise writes always — which sends an agent
20
+ * down paths that will `403` — or to let the operator say. This is the operator saying.
21
+ */
22
+ export const READ_ONLY_VAR = 'EBITEX_CONTENT_MCP_READ_ONLY';
23
+ /** A second key, bound to the environment `content_transfer` imports *into*. */
24
+ export const TARGET_KEY_VAR = 'EBITEX_CONTENT_TARGET_MANAGEMENT_KEY';
25
+ export const DEFAULT_BASE = 'https://api.ebitex.io';
26
+ export class ConfigError extends Error {
27
+ }
28
+ /**
29
+ * Reads the configuration, or throws {@link ConfigError} with a message written for the person
30
+ * editing an MCP client's JSON — it names the variable and where to get its value, because that is
31
+ * the only thing they can act on.
32
+ */
33
+ export function readConfig(env = process.env) {
34
+ const key = env[KEY_VAR]?.trim();
35
+ if (!key) {
36
+ throw new ConfigError(`${KEY_VAR} is not set. Mint a Content management key in Composer ` +
37
+ '(Settings > Management keys) and set it in this server\'s "env" block.');
38
+ }
39
+ const base = env[BASE_VAR]?.trim() || DEFAULT_BASE;
40
+ let baseUrl;
41
+ try {
42
+ // Normalizing here rather than at each call site means a trailing slash, a path, or a typo in
43
+ // the scheme is one error at startup instead of a puzzling 404 per tool.
44
+ baseUrl = new URL(base).origin;
45
+ }
46
+ catch {
47
+ throw new ConfigError(`${BASE_VAR} is not a valid URL: ${base}`);
48
+ }
49
+ return {
50
+ baseUrl,
51
+ key,
52
+ targetKey: env[TARGET_KEY_VAR]?.trim() || null,
53
+ readOnly: isTruthy(env[READ_ONLY_VAR]),
54
+ };
55
+ }
56
+ function isTruthy(value) {
57
+ if (!value)
58
+ return false;
59
+ const normalized = value.trim().toLowerCase();
60
+ return normalized === '1' || normalized === 'true' || normalized === 'yes';
61
+ }
62
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,sFAAsF;AACtF,MAAM,CAAC,MAAM,OAAO,GAAG,+BAA+B,CAAA;AAEtD,8FAA8F;AAC9F,MAAM,CAAC,MAAM,QAAQ,GAAG,yBAAyB,CAAA;AAEjD;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,aAAa,GAAG,8BAA8B,CAAA;AAE3D,gFAAgF;AAChF,MAAM,CAAC,MAAM,cAAc,GAAG,sCAAsC,CAAA;AAEpE,MAAM,CAAC,MAAM,YAAY,GAAG,uBAAuB,CAAA;AAWnD,MAAM,OAAO,WAAY,SAAQ,KAAK;CAAG;AAEzC;;;;GAIG;AACH,MAAM,UAAU,UAAU,CAAC,MAA0C,OAAO,CAAC,GAAG;IAC9E,MAAM,GAAG,GAAG,GAAG,CAAC,OAAO,CAAC,EAAE,IAAI,EAAE,CAAA;IAChC,IAAI,CAAC,GAAG,EAAE,CAAC;QACT,MAAM,IAAI,WAAW,CACnB,GAAG,OAAO,yDAAyD;YACjE,wEAAwE,CAC3E,CAAA;IACH,CAAC;IAED,MAAM,IAAI,GAAG,GAAG,CAAC,QAAQ,CAAC,EAAE,IAAI,EAAE,IAAI,YAAY,CAAA;IAClD,IAAI,OAAe,CAAA;IACnB,IAAI,CAAC;QACH,8FAA8F;QAC9F,yEAAyE;QACzE,OAAO,GAAG,IAAI,GAAG,CAAC,IAAI,CAAC,CAAC,MAAM,CAAA;IAChC,CAAC;IAAC,MAAM,CAAC;QACP,MAAM,IAAI,WAAW,CAAC,GAAG,QAAQ,wBAAwB,IAAI,EAAE,CAAC,CAAA;IAClE,CAAC;IAED,OAAO;QACL,OAAO;QACP,GAAG;QACH,SAAS,EAAE,GAAG,CAAC,cAAc,CAAC,EAAE,IAAI,EAAE,IAAI,IAAI;QAC9C,QAAQ,EAAE,QAAQ,CAAC,GAAG,CAAC,aAAa,CAAC,CAAC;KACvC,CAAA;AACH,CAAC;AAED,SAAS,QAAQ,CAAC,KAAyB;IACzC,IAAI,CAAC,KAAK;QAAE,OAAO,KAAK,CAAA;IACxB,MAAM,UAAU,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAA;IAC7C,OAAO,UAAU,KAAK,GAAG,IAAI,UAAU,KAAK,MAAM,IAAI,UAAU,KAAK,KAAK,CAAA;AAC5E,CAAC"}
@@ -0,0 +1,43 @@
1
+ /**
2
+ * How a refusal from the API reaches the agent — SPEC.md's Business Logic §3.
3
+ *
4
+ * The rule is that the error **is** the control loop. An agent works by try → read the error → fix,
5
+ * and that loop only closes if the error still says what to change. Content's per-entity write
6
+ * refuses an invalid document with the failing field's path and message; a bundle import names every
7
+ * invalid item. So this module's entire job is to lose none of that.
8
+ *
9
+ * Concretely, it does **not** branch on the error code, and that is deliberate rather than lazy:
10
+ * `apps/content-web/src/lib/contentApi.ts` has a large switch mapping each code to a friendly
11
+ * sentence, which is right for a person looking at a form and wrong here twice over — it discards
12
+ * the field paths a program needs, and it goes stale the first time the API adds a code (that file
13
+ * has had exactly that bug, twice, where an unmapped code fell through to a branch that dropped the
14
+ * body entirely). Emitting the whole body cannot go stale, because it makes no claim about shape.
15
+ *
16
+ * One shaping *is* applied, and only one: the body is pretty-printed. That changes no content.
17
+ */
18
+ /** A non-2xx response, carrying everything the server said about it. */
19
+ export declare class ContentApiError extends Error {
20
+ readonly status: number;
21
+ /** The API's own `error` code when the body carries one — `validation_failed`, `scope_denied`, … */
22
+ readonly code: string | null;
23
+ /** The parsed body, or the raw text when it was not JSON (a proxy's HTML error page, say). */
24
+ readonly body: unknown;
25
+ constructor(status: number, code: string | null, body: unknown, message: string);
26
+ }
27
+ /**
28
+ * Builds the error from a response, reading the body once.
29
+ *
30
+ * A body that is not JSON is kept as text rather than discarded: when something in front of the API
31
+ * answers (a proxy timeout, a WAF), that text is the only evidence of what happened, and an empty
32
+ * error would send the agent looking at its own request.
33
+ */
34
+ export declare function toContentApiError(response: Response): Promise<ContentApiError>;
35
+ /**
36
+ * The text an agent actually reads.
37
+ *
38
+ * Line one is the status and the API's own code, so the machine-readable identity is never buried.
39
+ * Everything after it is the body, verbatim. No summary, no count, no "and 3 more" — an elision
40
+ * here would remove exactly the field the next call has to set.
41
+ */
42
+ export declare function describe(status: number, code: string | null, body: unknown): string;
43
+ //# sourceMappingURL=errors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,wEAAwE;AACxE,qBAAa,eAAgB,SAAQ,KAAK;IACxC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,oGAAoG;IACpG,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAA;IAC5B,8FAA8F;IAC9F,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAA;gBAEV,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI,EAAE,IAAI,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM;CAOhF;AAED;;;;;;GAMG;AACH,wBAAsB,iBAAiB,CAAC,QAAQ,EAAE,QAAQ,GAAG,OAAO,CAAC,eAAe,CAAC,CAiBpF;AAED;;;;;;GAMG;AACH,wBAAgB,QAAQ,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI,EAAE,IAAI,EAAE,OAAO,GAAG,MAAM,CAMnF"}
package/dist/errors.js ADDED
@@ -0,0 +1,82 @@
1
+ /**
2
+ * How a refusal from the API reaches the agent — SPEC.md's Business Logic §3.
3
+ *
4
+ * The rule is that the error **is** the control loop. An agent works by try → read the error → fix,
5
+ * and that loop only closes if the error still says what to change. Content's per-entity write
6
+ * refuses an invalid document with the failing field's path and message; a bundle import names every
7
+ * invalid item. So this module's entire job is to lose none of that.
8
+ *
9
+ * Concretely, it does **not** branch on the error code, and that is deliberate rather than lazy:
10
+ * `apps/content-web/src/lib/contentApi.ts` has a large switch mapping each code to a friendly
11
+ * sentence, which is right for a person looking at a form and wrong here twice over — it discards
12
+ * the field paths a program needs, and it goes stale the first time the API adds a code (that file
13
+ * has had exactly that bug, twice, where an unmapped code fell through to a branch that dropped the
14
+ * body entirely). Emitting the whole body cannot go stale, because it makes no claim about shape.
15
+ *
16
+ * One shaping *is* applied, and only one: the body is pretty-printed. That changes no content.
17
+ */
18
+ /** A non-2xx response, carrying everything the server said about it. */
19
+ export class ContentApiError extends Error {
20
+ status;
21
+ /** The API's own `error` code when the body carries one — `validation_failed`, `scope_denied`, … */
22
+ code;
23
+ /** The parsed body, or the raw text when it was not JSON (a proxy's HTML error page, say). */
24
+ body;
25
+ constructor(status, code, body, message) {
26
+ super(message);
27
+ this.name = 'ContentApiError';
28
+ this.status = status;
29
+ this.code = code;
30
+ this.body = body;
31
+ }
32
+ }
33
+ /**
34
+ * Builds the error from a response, reading the body once.
35
+ *
36
+ * A body that is not JSON is kept as text rather than discarded: when something in front of the API
37
+ * answers (a proxy timeout, a WAF), that text is the only evidence of what happened, and an empty
38
+ * error would send the agent looking at its own request.
39
+ */
40
+ export async function toContentApiError(response) {
41
+ const text = await response.text().catch(() => '');
42
+ let body = text;
43
+ let code = null;
44
+ if (text) {
45
+ try {
46
+ body = JSON.parse(text);
47
+ if (body && typeof body === 'object' && typeof body.error === 'string') {
48
+ code = body.error;
49
+ }
50
+ }
51
+ catch {
52
+ // Not JSON. `body` stays the raw text.
53
+ }
54
+ }
55
+ return new ContentApiError(response.status, code, body, describe(response.status, code, body));
56
+ }
57
+ /**
58
+ * The text an agent actually reads.
59
+ *
60
+ * Line one is the status and the API's own code, so the machine-readable identity is never buried.
61
+ * Everything after it is the body, verbatim. No summary, no count, no "and 3 more" — an elision
62
+ * here would remove exactly the field the next call has to set.
63
+ */
64
+ export function describe(status, code, body) {
65
+ const heading = code ? `HTTP ${status} ${code}` : `HTTP ${status}`;
66
+ if (body === undefined || body === null || body === '')
67
+ return heading;
68
+ const rendered = typeof body === 'string' ? body : safeStringify(body);
69
+ return `${heading}\n${rendered}`;
70
+ }
71
+ function safeStringify(value) {
72
+ try {
73
+ return JSON.stringify(value, null, 2);
74
+ }
75
+ catch {
76
+ // A cycle cannot occur in a parsed JSON body, but stringify can still throw on a BigInt. Saying
77
+ // so beats throwing while reporting an error, which would replace a useful message with a
78
+ // useless one.
79
+ return String(value);
80
+ }
81
+ }
82
+ //# sourceMappingURL=errors.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.js","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,wEAAwE;AACxE,MAAM,OAAO,eAAgB,SAAQ,KAAK;IAC/B,MAAM,CAAQ;IACvB,oGAAoG;IAC3F,IAAI,CAAe;IAC5B,8FAA8F;IACrF,IAAI,CAAS;IAEtB,YAAY,MAAc,EAAE,IAAmB,EAAE,IAAa,EAAE,OAAe;QAC7E,KAAK,CAAC,OAAO,CAAC,CAAA;QACd,IAAI,CAAC,IAAI,GAAG,iBAAiB,CAAA;QAC7B,IAAI,CAAC,MAAM,GAAG,MAAM,CAAA;QACpB,IAAI,CAAC,IAAI,GAAG,IAAI,CAAA;QAChB,IAAI,CAAC,IAAI,GAAG,IAAI,CAAA;IAClB,CAAC;CACF;AAED;;;;;;GAMG;AACH,MAAM,CAAC,KAAK,UAAU,iBAAiB,CAAC,QAAkB;IACxD,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,EAAE,CAAC,CAAA;IAElD,IAAI,IAAI,GAAY,IAAI,CAAA;IACxB,IAAI,IAAI,GAAkB,IAAI,CAAA;IAC9B,IAAI,IAAI,EAAE,CAAC;QACT,IAAI,CAAC;YACH,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAA;YACvB,IAAI,IAAI,IAAI,OAAO,IAAI,KAAK,QAAQ,IAAI,OAAQ,IAA4B,CAAC,KAAK,KAAK,QAAQ,EAAE,CAAC;gBAChG,IAAI,GAAI,IAA0B,CAAC,KAAK,CAAA;YAC1C,CAAC;QACH,CAAC;QAAC,MAAM,CAAC;YACP,uCAAuC;QACzC,CAAC;IACH,CAAC;IAED,OAAO,IAAI,eAAe,CAAC,QAAQ,CAAC,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,QAAQ,CAAC,QAAQ,CAAC,MAAM,EAAE,IAAI,EAAE,IAAI,CAAC,CAAC,CAAA;AAChG,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,QAAQ,CAAC,MAAc,EAAE,IAAmB,EAAE,IAAa;IACzE,MAAM,OAAO,GAAG,IAAI,CAAC,CAAC,CAAC,QAAQ,MAAM,IAAI,IAAI,EAAE,CAAC,CAAC,CAAC,QAAQ,MAAM,EAAE,CAAA;IAClE,IAAI,IAAI,KAAK,SAAS,IAAI,IAAI,KAAK,IAAI,IAAI,IAAI,KAAK,EAAE;QAAE,OAAO,OAAO,CAAA;IAEtE,MAAM,QAAQ,GAAG,OAAO,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,aAAa,CAAC,IAAI,CAAC,CAAA;IACtE,OAAO,GAAG,OAAO,KAAK,QAAQ,EAAE,CAAA;AAClC,CAAC;AAED,SAAS,aAAa,CAAC,KAAc;IACnC,IAAI,CAAC;QACH,OAAO,IAAI,CAAC,SAAS,CAAC,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC,CAAA;IACvC,CAAC;IAAC,MAAM,CAAC;QACP,gGAAgG;QAChG,0FAA0F;QAC1F,eAAe;QACf,OAAO,MAAM,CAAC,KAAK,CAAC,CAAA;IACtB,CAAC;AACH,CAAC"}
@@ -0,0 +1,14 @@
1
+ /**
2
+ * The library entry point, for embedding this server in a host that owns its own transport.
3
+ *
4
+ * The ordinary way to use this package is the `ebitex-content-mcp` executable, configured in an MCP
5
+ * client. This export exists so a host that already has a transport (an in-process client, an HTTP
6
+ * bridge) can build the same server without going through a child process.
7
+ */
8
+ export { buildServer, startupBanner, SERVER_NAME, SERVER_VERSION, type BuiltServer } from './server.js';
9
+ export { readConfig, ConfigError, type ServerConfig } from './config.js';
10
+ export { resolveSession, StartupError, type Session, type WhoAmI } from './session.js';
11
+ export { createClient, type ContentClient } from './client.js';
12
+ export { ContentApiError } from './errors.js';
13
+ export { WRITABLE_KINDS, isEntityKind, routesFor, type EntityKind } from './routes.js';
14
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,EAAE,WAAW,EAAE,aAAa,EAAE,WAAW,EAAE,cAAc,EAAE,KAAK,WAAW,EAAE,MAAM,aAAa,CAAA;AACvG,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,KAAK,YAAY,EAAE,MAAM,aAAa,CAAA;AACxE,OAAO,EAAE,cAAc,EAAE,YAAY,EAAE,KAAK,OAAO,EAAE,KAAK,MAAM,EAAE,MAAM,cAAc,CAAA;AACtF,OAAO,EAAE,YAAY,EAAE,KAAK,aAAa,EAAE,MAAM,aAAa,CAAA;AAC9D,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAA;AAC7C,OAAO,EAAE,cAAc,EAAE,YAAY,EAAE,SAAS,EAAE,KAAK,UAAU,EAAE,MAAM,aAAa,CAAA"}
package/dist/index.js ADDED
@@ -0,0 +1,14 @@
1
+ /**
2
+ * The library entry point, for embedding this server in a host that owns its own transport.
3
+ *
4
+ * The ordinary way to use this package is the `ebitex-content-mcp` executable, configured in an MCP
5
+ * client. This export exists so a host that already has a transport (an in-process client, an HTTP
6
+ * bridge) can build the same server without going through a child process.
7
+ */
8
+ export { buildServer, startupBanner, SERVER_NAME, SERVER_VERSION } from './server.js';
9
+ export { readConfig, ConfigError } from './config.js';
10
+ export { resolveSession, StartupError } from './session.js';
11
+ export { createClient } from './client.js';
12
+ export { ContentApiError } from './errors.js';
13
+ export { WRITABLE_KINDS, isEntityKind, routesFor } from './routes.js';
14
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,EAAE,WAAW,EAAE,aAAa,EAAE,WAAW,EAAE,cAAc,EAAoB,MAAM,aAAa,CAAA;AACvG,OAAO,EAAE,UAAU,EAAE,WAAW,EAAqB,MAAM,aAAa,CAAA;AACxE,OAAO,EAAE,cAAc,EAAE,YAAY,EAA6B,MAAM,cAAc,CAAA;AACtF,OAAO,EAAE,YAAY,EAAsB,MAAM,aAAa,CAAA;AAC9D,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAA;AAC7C,OAAO,EAAE,cAAc,EAAE,YAAY,EAAE,SAAS,EAAmB,MAAM,aAAa,CAAA"}
package/dist/main.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=main.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"main.d.ts","sourceRoot":"","sources":["../src/main.ts"],"names":[],"mappings":""}
package/dist/main.js ADDED
@@ -0,0 +1,35 @@
1
+ #!/usr/bin/env node
2
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
3
+ import { ConfigError } from './config.js';
4
+ import { buildServer } from './server.js';
5
+ import { StartupError } from './session.js';
6
+ /**
7
+ * The published `ebitex-content-mcp` executable. `package.json`'s `bin` points at this module's
8
+ * compiled output; TypeScript preserves the leading `#!` verbatim on emit, so there is no separate
9
+ * untyped shim.
10
+ *
11
+ * Kept to process concerns only — starting, failing, exiting — with everything testable living in
12
+ * `server.ts`.
13
+ *
14
+ * **Everything diagnostic goes to stderr.** On a stdio transport, stdout *is* the protocol: a single
15
+ * stray `console.log` corrupts the JSON-RPC stream and the client reports a parse error rather than
16
+ * whatever was printed. That is the one rule to remember when adding anything to this file.
17
+ */
18
+ async function main() {
19
+ const { server, banner } = await buildServer();
20
+ console.error(banner);
21
+ await server.connect(new StdioServerTransport());
22
+ }
23
+ main().catch((error) => {
24
+ // A configuration or startup failure is something a person can fix in a config file, so it is
25
+ // printed as its message alone — a stack trace here would bury the one line that matters.
26
+ // Anything else is a real fault and keeps its stack.
27
+ if (error instanceof ConfigError || error instanceof StartupError) {
28
+ console.error(error.message);
29
+ }
30
+ else {
31
+ console.error(error);
32
+ }
33
+ process.exit(1);
34
+ });
35
+ //# sourceMappingURL=main.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"main.js","sourceRoot":"","sources":["../src/main.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,oBAAoB,EAAE,MAAM,2CAA2C,CAAA;AAChF,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAA;AACzC,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAA;AACzC,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAA;AAE3C;;;;;;;;;;;GAWG;AACH,KAAK,UAAU,IAAI;IACjB,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,GAAG,MAAM,WAAW,EAAE,CAAA;IAC9C,OAAO,CAAC,KAAK,CAAC,MAAM,CAAC,CAAA;IAErB,MAAM,MAAM,CAAC,OAAO,CAAC,IAAI,oBAAoB,EAAE,CAAC,CAAA;AAClD,CAAC;AAED,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,KAAc,EAAE,EAAE;IAC9B,8FAA8F;IAC9F,0FAA0F;IAC1F,qDAAqD;IACrD,IAAI,KAAK,YAAY,WAAW,IAAI,KAAK,YAAY,YAAY,EAAE,CAAC;QAClE,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,OAAO,CAAC,CAAA;IAC9B,CAAC;SAAM,CAAC;QACN,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,CAAA;IACtB,CAAC;IAED,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;AACjB,CAAC,CAAC,CAAA"}
@@ -0,0 +1,7 @@
1
+ export type HelpTopic = {
2
+ slug: string;
3
+ title: string;
4
+ body: string;
5
+ };
6
+ export declare const HELP_TOPICS: HelpTopic[];
7
+ //# sourceMappingURL=helpCorpus.generated.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"helpCorpus.generated.d.ts","sourceRoot":"","sources":["../../src/resources/helpCorpus.generated.ts"],"names":[],"mappings":"AAKA,MAAM,MAAM,SAAS,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAA;AAErE,eAAO,MAAM,WAAW,EAAE,SAAS,EA0GlC,CAAA"}