@a5c-ai/channels-adapter 5.1.1-staging.5ee7f5f98b91

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 (71) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +661 -0
  3. package/dist/backend.d.ts +7 -0
  4. package/dist/backend.d.ts.map +1 -0
  5. package/dist/backend.js +22 -0
  6. package/dist/backend.js.map +1 -0
  7. package/dist/backends/github.d.ts +16 -0
  8. package/dist/backends/github.d.ts.map +1 -0
  9. package/dist/backends/github.js +357 -0
  10. package/dist/backends/github.js.map +1 -0
  11. package/dist/backends/jira.d.ts +18 -0
  12. package/dist/backends/jira.d.ts.map +1 -0
  13. package/dist/backends/jira.js +256 -0
  14. package/dist/backends/jira.js.map +1 -0
  15. package/dist/backends/webhook.d.ts +25 -0
  16. package/dist/backends/webhook.d.ts.map +1 -0
  17. package/dist/backends/webhook.js +206 -0
  18. package/dist/backends/webhook.js.map +1 -0
  19. package/dist/cli.d.ts +2 -0
  20. package/dist/cli.d.ts.map +1 -0
  21. package/dist/cli.js +28 -0
  22. package/dist/cli.js.map +1 -0
  23. package/dist/config.d.ts +10 -0
  24. package/dist/config.d.ts.map +1 -0
  25. package/dist/config.js +323 -0
  26. package/dist/config.js.map +1 -0
  27. package/dist/dedup.d.ts +26 -0
  28. package/dist/dedup.d.ts.map +1 -0
  29. package/dist/dedup.js +68 -0
  30. package/dist/dedup.js.map +1 -0
  31. package/dist/filter.d.ts +9 -0
  32. package/dist/filter.d.ts.map +1 -0
  33. package/dist/filter.js +115 -0
  34. package/dist/filter.js.map +1 -0
  35. package/dist/index.d.ts +14 -0
  36. package/dist/index.d.ts.map +1 -0
  37. package/dist/index.js +21 -0
  38. package/dist/index.js.map +1 -0
  39. package/dist/poller.d.ts +63 -0
  40. package/dist/poller.d.ts.map +1 -0
  41. package/dist/poller.js +195 -0
  42. package/dist/poller.js.map +1 -0
  43. package/dist/registry.d.ts +22 -0
  44. package/dist/registry.d.ts.map +1 -0
  45. package/dist/registry.js +59 -0
  46. package/dist/registry.js.map +1 -0
  47. package/dist/relay.d.ts +58 -0
  48. package/dist/relay.d.ts.map +1 -0
  49. package/dist/relay.js +172 -0
  50. package/dist/relay.js.map +1 -0
  51. package/dist/runtime.d.ts +27 -0
  52. package/dist/runtime.d.ts.map +1 -0
  53. package/dist/runtime.js +198 -0
  54. package/dist/runtime.js.map +1 -0
  55. package/dist/server.d.ts +64 -0
  56. package/dist/server.d.ts.map +1 -0
  57. package/dist/server.js +217 -0
  58. package/dist/server.js.map +1 -0
  59. package/dist/spawner.d.ts +96 -0
  60. package/dist/spawner.d.ts.map +1 -0
  61. package/dist/spawner.js +368 -0
  62. package/dist/spawner.js.map +1 -0
  63. package/dist/state.d.ts +43 -0
  64. package/dist/state.d.ts.map +1 -0
  65. package/dist/state.js +92 -0
  66. package/dist/state.js.map +1 -0
  67. package/dist/types.d.ts +140 -0
  68. package/dist/types.d.ts.map +1 -0
  69. package/dist/types.js +10 -0
  70. package/dist/types.js.map +1 -0
  71. package/package.json +78 -0
@@ -0,0 +1,198 @@
1
+ // Runtime composition root (SPEC §3/§6, DESIGN §1/§3).
2
+ //
3
+ // createRuntime(configPathOrYaml, deps) loads + validates config, constructs the
4
+ // ChannelServer, resolves backends (built-in via the registry, custom via a
5
+ // relative path), wires the Poller, and wires the reply tool back through the
6
+ // opaque reply_to token to the owning backend's reply(). Returns
7
+ // { server, poller, config, start, stop }.
8
+ //
9
+ // start(): connect the (optional) transport + start the poller.
10
+ // stop(): stop the poller + close the server.
11
+ //
12
+ // `deps` injects the seams the tests need: a fake `http`, an in-memory
13
+ // `stateStore`, a fixed `now`, and/or a capturing `transport`.
14
+ import { existsSync, statSync } from 'node:fs';
15
+ import { resolve } from 'node:path';
16
+ import { loadConfig } from './config.js';
17
+ import { ChannelServer } from './server.js';
18
+ import { Poller } from './poller.js';
19
+ import { StateStore } from './state.js';
20
+ import { registry } from './registry.js';
21
+ import { createRelay } from './relay.js';
22
+ import { SessionSpawner } from './spawner.js';
23
+ /** A backend reference is a custom-module path (not a built-in type) when it
24
+ * looks like a relative/absolute path or ends in a JS extension. */
25
+ function looksLikePath(backend) {
26
+ return (typeof backend === 'string' &&
27
+ (backend.startsWith('.') ||
28
+ backend.startsWith('/') ||
29
+ /\.[cm]?js$/.test(backend) ||
30
+ /^[A-Za-z]:[\\/]/.test(backend)));
31
+ }
32
+ export async function createRuntime(configPathOrYaml, deps = {}) {
33
+ const { config, errors } = loadConfig(configPathOrYaml, { env: deps.env });
34
+ if (errors.length > 0) {
35
+ throw new Error(`mcp-channels: invalid config:\n - ${errors.join('\n - ')}`);
36
+ }
37
+ const http = deps.http || ((url, opts) => fetch(url, opts));
38
+ const now = deps.now || (() => new Date());
39
+ // Resolve the effective reply secret (DESIGN §7.4): config wins, env is the
40
+ // fallback. When set, a bound relay signs AND verifies with the derived key so
41
+ // the running instance interoperates with a child sharing the secret (both
42
+ // directions). When unset, the bound relay degrades to the per-process random
43
+ // key — identical to today's behavior (backward compatible).
44
+ const replySecret = config.server.replySecret ||
45
+ (deps.env ? deps.env.MCP_CHANNELS_REPLY_SECRET : process.env.MCP_CHANNELS_REPLY_SECRET) ||
46
+ undefined;
47
+ const relay = createRelay(replySecret);
48
+ const stateStore = deps.stateStore ||
49
+ new StateStore({
50
+ dir: config.state.dir || defaultStateDir(config.server.name),
51
+ maxSeenPerSource: config.state.maxSeenPerSource
52
+ });
53
+ const server = new ChannelServer({
54
+ name: config.server.name,
55
+ instructions: config.server.instructions,
56
+ permissionRelay: config.server.permissionRelay
57
+ });
58
+ // Wire a default permission handler when the relay is enabled (finding §13).
59
+ if (config.server.permissionRelay) {
60
+ server.setPermissionRequestHandler(async (req) => {
61
+ let behavior = 'deny';
62
+ try {
63
+ if (typeof deps.permissionHandler === 'function') {
64
+ const decision = await deps.permissionHandler(req);
65
+ behavior = decision === 'allow' ? 'allow' : 'deny';
66
+ }
67
+ }
68
+ catch {
69
+ behavior = 'deny';
70
+ }
71
+ await server.emitPermission({ request_id: req?.request_id, behavior });
72
+ });
73
+ }
74
+ // Cache resolved backends per source id (custom backends are imported once).
75
+ const backendCache = new Map();
76
+ const sourcesById = new Map(config.sources.map((s) => [s.id, s]));
77
+ async function resolveBackend(source) {
78
+ if (backendCache.has(source.id))
79
+ return backendCache.get(source.id);
80
+ let backend;
81
+ if (looksLikePath(source.backend)) {
82
+ backend = await registry.load(source.backend, config.baseDir);
83
+ }
84
+ else {
85
+ backend = registry.get(source.backend);
86
+ }
87
+ if (backend)
88
+ backendCache.set(source.id, backend);
89
+ return backend;
90
+ }
91
+ // Eagerly resolve + validate custom-path backends at startup so a broken
92
+ // custom backend (missing poll/reply, bad import) fails createRuntime rather
93
+ // than the first tick.
94
+ for (const source of config.sources) {
95
+ if (looksLikePath(source.backend)) {
96
+ await resolveBackend(source); // throws -> propagates out of createRuntime
97
+ }
98
+ }
99
+ // Wire the reply tool through relay.dispatchReply — the SINGLE source of truth
100
+ // for the reply path (decode token -> owning source + backend -> backend.reply).
101
+ server.setReplyHandler(async ({ reply_to, text }) => relay.dispatchReply({
102
+ reply_to,
103
+ text,
104
+ http,
105
+ resolveSource: (id) => sourcesById.get(id),
106
+ // Resolve the reply backend from the token's OWN sourceId, not by scanning
107
+ // sources for a matching `type`.
108
+ resolveBackend: async (_backendType, sourceId) => {
109
+ const source = sourcesById.get(sourceId);
110
+ if (!source)
111
+ return undefined;
112
+ return resolveBackend(source);
113
+ }
114
+ }));
115
+ // Construct a SessionSpawner only when a source actually opts into spawning
116
+ // (DESIGN §7.1).
117
+ const wantsSpawn = config.sources.some((s) => s.onEvent === 'spawn' || s.onEvent === 'both');
118
+ let spawner = null;
119
+ if (wantsSpawn) {
120
+ spawner = new SessionSpawner({
121
+ client: deps.client,
122
+ loadClient: deps.loadClient,
123
+ configPath: resolveConfigPath(configPathOrYaml),
124
+ replySecret,
125
+ maxConcurrent: config.spawn?.maxConcurrent,
126
+ resolveCliPath: deps.resolveCliPath,
127
+ log: deps.log || (() => { })
128
+ });
129
+ // AC-25: a source needs spawn but no client is injected and the optional dep
130
+ // can't be obtained -> fail STARTUP clearly, not at event time.
131
+ await spawner.validate();
132
+ }
133
+ const poller = new Poller({
134
+ sources: config.sources,
135
+ resolveBackend,
136
+ stateStore,
137
+ server,
138
+ spawner,
139
+ // Mint reply_to under the bound relay so a child sharing the secret can
140
+ // decode tokens this instance emitted (DESIGN §7.6).
141
+ encodeReplyTo: relay.encodeReplyTo,
142
+ http,
143
+ now,
144
+ log: () => { }
145
+ });
146
+ let started = false;
147
+ return {
148
+ server,
149
+ poller,
150
+ config,
151
+ errors,
152
+ async start() {
153
+ if (started)
154
+ return;
155
+ started = true;
156
+ if (deps.transport) {
157
+ await server.connect(deps.transport);
158
+ }
159
+ poller.start();
160
+ },
161
+ async stop() {
162
+ poller.stop();
163
+ if (started) {
164
+ try {
165
+ await server.close();
166
+ }
167
+ catch {
168
+ // ignore close errors during teardown
169
+ }
170
+ }
171
+ started = false;
172
+ }
173
+ };
174
+ }
175
+ /**
176
+ * Resolve the config path for the self-MCP re-launch (DESIGN §7.3): an existing
177
+ * file path is resolved ABSOLUTELY (so the child finds it regardless of cwd);
178
+ * inline YAML (which has no path on disk) is passed through unchanged.
179
+ */
180
+ function resolveConfigPath(input) {
181
+ if (typeof input === 'string' && !input.includes('\n')) {
182
+ try {
183
+ if (existsSync(input) && statSync(input).isFile()) {
184
+ return resolve(input);
185
+ }
186
+ }
187
+ catch {
188
+ // fall through — treat as inline / non-file
189
+ }
190
+ }
191
+ return input;
192
+ }
193
+ /** Default per-server state dir under ~/.claude/channels/<name>/state. */
194
+ function defaultStateDir(name) {
195
+ const home = process.env.HOME || process.env.USERPROFILE || process.cwd();
196
+ return `${home}/.claude/channels/${name || 'mcp-channels'}/state`;
197
+ }
198
+ //# sourceMappingURL=runtime.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"runtime.js","sourceRoot":"","sources":["../src/runtime.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,64 @@
1
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
2
+ import type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js';
3
+ import type { ReplyResult } from './types.js';
4
+ /** A reply handler invoked when Claude calls the `reply` tool. */
5
+ export type ReplyHandler = (a: {
6
+ reply_to: string;
7
+ text: string;
8
+ }) => Promise<ReplyResult>;
9
+ /** A permission-request handler invoked on an inbound permission_request. */
10
+ export type PermissionRequestHandler = (req: {
11
+ request_id: string;
12
+ tool_name?: string;
13
+ description?: string;
14
+ input_preview?: string;
15
+ }) => void | Promise<void>;
16
+ export declare const DEFAULT_INSTRUCTIONS: string;
17
+ export declare class ChannelServer {
18
+ name: string;
19
+ permissionRelay: boolean;
20
+ instructions: string;
21
+ server: Server;
22
+ _replyHandler: ReplyHandler | null;
23
+ _permissionRequestHandler: PermissionRequestHandler | null;
24
+ constructor({ name, instructions, permissionRelay }?: {
25
+ name: string;
26
+ instructions?: string;
27
+ permissionRelay?: boolean;
28
+ });
29
+ /** Register the `reply` tool handlers (ListTools + CallTool). */
30
+ _registerReplyTool(): void;
31
+ /** Wire the inbound permission_request notification handler. */
32
+ _registerPermissionRelay(): void;
33
+ /**
34
+ * Set the handler invoked when Claude calls the `reply` tool.
35
+ */
36
+ setReplyHandler(fn: ReplyHandler): void;
37
+ /**
38
+ * Set the handler invoked on an inbound permission_request (relay mode).
39
+ */
40
+ setPermissionRequestHandler(fn: PermissionRequestHandler): void;
41
+ /**
42
+ * Emit one channel event as a notifications/claude/channel.
43
+ */
44
+ emit({ content, meta }?: {
45
+ content?: string;
46
+ meta?: Record<string, unknown>;
47
+ }): Promise<void>;
48
+ /**
49
+ * Emit a permission decision back to Claude (relay mode). `behavior` is coerced
50
+ * to EXACTLY 'allow' or 'deny' so the SPEC §2 contract holds regardless of what
51
+ * the caller passes — any value other than the literal 'allow' becomes 'deny'
52
+ * (silence/garbage means deny, the safe stance for an untrusted-input gate).
53
+ */
54
+ emitPermission({ request_id, behavior }: {
55
+ request_id: string;
56
+ behavior: 'allow' | 'deny';
57
+ }): Promise<void>;
58
+ /**
59
+ * Connect the underlying MCP server to a transport.
60
+ */
61
+ connect(transport: Transport): Promise<void>;
62
+ /** Close the underlying MCP server. */
63
+ close(): Promise<void>;
64
+ }
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAeA,OAAO,EAAE,MAAM,EAAE,MAAM,2CAA2C,CAAC;AAKnE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,+CAA+C,CAAC;AAC/E,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAe9C,kEAAkE;AAClE,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,EAAE;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,KAAK,OAAO,CAAC,WAAW,CAAC,CAAC;AAE3F,6EAA6E;AAC7E,MAAM,MAAM,wBAAwB,GAAG,CAAC,GAAG,EAAE;IAC3C,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;AAyB3B,eAAO,MAAM,oBAAoB,QAQnB,CAAC;AAEf,qBAAa,aAAa;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,eAAe,EAAE,OAAO,CAAC;IACzB,YAAY,EAAE,MAAM,CAAC;IACrB,MAAM,EAAE,MAAM,CAAC;IACf,aAAa,EAAE,YAAY,GAAG,IAAI,CAAC;IACnC,yBAAyB,EAAE,wBAAwB,GAAG,IAAI,CAAC;gBAE/C,EACV,IAAI,EACJ,YAAY,EACZ,eAAuB,EACxB,GAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,YAAY,CAAC,EAAE,MAAM,CAAC;QAAC,eAAe,CAAC,EAAE,OAAO,CAAA;KAIlE;IAoCD,iEAAiE;IACjE,kBAAkB,IAAI,IAAI;IA8D1B,gEAAgE;IAChE,wBAAwB,IAAI,IAAI;IAuBhC;;OAEG;IACH,eAAe,CAAC,EAAE,EAAE,YAAY,GAAG,IAAI;IAIvC;;OAEG;IACH,2BAA2B,CAAC,EAAE,EAAE,wBAAwB,GAAG,IAAI;IAI/D;;OAEG;IACG,IAAI,CAAC,EACT,OAAO,EACP,IAAI,EACL,GAAE;QAAE,OAAO,CAAC,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;KAAO,GAAG,OAAO,CAAC,IAAI,CAAC;IAU5E;;;;;OAKG;IACG,cAAc,CAAC,EACnB,UAAU,EACV,QAAQ,EACT,EAAE;QACD,UAAU,EAAE,MAAM,CAAC;QACnB,QAAQ,EAAE,OAAO,GAAG,MAAM,CAAC;KAC5B,GAAG,OAAO,CAAC,IAAI,CAAC;IAOjB;;OAEG;IACG,OAAO,CAAC,SAAS,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC;IAIlD,uCAAuC;IACjC,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;CAG7B"}
package/dist/server.js ADDED
@@ -0,0 +1,217 @@
1
+ // ChannelServer — wraps the MCP `Server` to implement the claude/channel
2
+ // contract (SPEC §2/§6 R1, DESIGN §1).
3
+ //
4
+ // - Declares channel capabilities: always experimental['claude/channel']={}
5
+ // and tools={}; experimental['claude/channel/permission']={} when the
6
+ // optional permission relay is enabled.
7
+ // - emit({content,meta}) sends exactly ONE notifications/claude/channel with
8
+ // SANITIZED meta (identifier keys only) and never a `source` key.
9
+ // - Registers the `reply` tool (ListTools + CallTool); a reply handler returns
10
+ // { ok, ref? } and the tool maps falsy ok / a throw to { isError:true }.
11
+ // - Transport-injectable via connect(transport) so tests capture the wire.
12
+ // - Optional permission relay: handles inbound permission_request and exposes
13
+ // emitPermission({request_id, behavior}).
14
+ import { z } from 'zod';
15
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
16
+ import { ListToolsRequestSchema, CallToolRequestSchema } from '@modelcontextprotocol/sdk/types.js';
17
+ const CHANNEL_NOTIFICATION = 'notifications/claude/channel';
18
+ const PERMISSION_NOTIFICATION = 'notifications/claude/channel/permission';
19
+ const PERMISSION_REQUEST_NOTIFICATION = 'notifications/claude/channel/permission_request';
20
+ /** A meta key is valid iff it is a bare identifier: ^[A-Za-z0-9_]+$ */
21
+ const IDENTIFIER_RE = /^[A-Za-z0-9_]+$/;
22
+ // Strip control chars (incl. newlines/tabs/DEL), double/single quotes, and angle
23
+ // brackets from meta VALUES so a value can never break the wire
24
+ // `<channel k="v" ...>` attribute encoding or smuggle markup into the channel tag.
25
+ // Built from explicit hex escapes to avoid literal control bytes in source.
26
+ const UNSAFE_VALUE_CHARS = new RegExp("[\u0000-\u001F\u007F\"'<>]", "g");
27
+ /**
28
+ * Sanitize meta into a strict `Record<string,string>`:
29
+ * - drop keys that are not bare identifiers `[A-Za-z0-9_]+`,
30
+ * - never emit a `source` key (Claude sets `source` from the server name),
31
+ * - drop entries whose value is null/undefined,
32
+ * - coerce every kept value to a String and strip control / quote / angle-bracket
33
+ * characters so the wire attribute stays clean (SPEC §2, finding §11).
34
+ */
35
+ function sanitizeMeta(meta) {
36
+ const out = {};
37
+ if (!meta || typeof meta !== 'object')
38
+ return out;
39
+ for (const [k, v] of Object.entries(meta)) {
40
+ if (k === 'source')
41
+ continue;
42
+ if (!IDENTIFIER_RE.test(k))
43
+ continue;
44
+ if (v === null || v === undefined)
45
+ continue;
46
+ out[k] = String(v).replace(UNSAFE_VALUE_CHARS, '');
47
+ }
48
+ return out;
49
+ }
50
+ // Default server `instructions` (SPEC §5, finding §12). Surfaced to Claude when
51
+ // the config does not override them; explains how channel events arrive and how
52
+ // to respond to the origin via the `reply` tool + the event's `reply_to` token.
53
+ export const DEFAULT_INSTRUCTIONS = 'This MCP server is a Claude Code *channel*. External events (e.g. GitHub or ' +
54
+ 'Jira activity) arrive as <channel source="..." ...>content</channel> tags. ' +
55
+ 'Each event carries a `reply_to` attribute — an opaque routing token. To respond ' +
56
+ 'back to the origin of an event (post a comment on the originating issue/PR or ' +
57
+ 'Jira issue), call the `reply` tool with that event\'s exact `reply_to` value and ' +
58
+ 'your `text`. Do not invent or modify `reply_to`; echo it verbatim. The token is ' +
59
+ 'opaque and carries no secrets — it only tells the framework where your reply ' +
60
+ 'should go.';
61
+ export class ChannelServer {
62
+ name;
63
+ permissionRelay;
64
+ instructions;
65
+ server;
66
+ _replyHandler;
67
+ _permissionRequestHandler;
68
+ constructor({ name, instructions, permissionRelay = false } = {}) {
69
+ this.name = name;
70
+ this.permissionRelay = !!permissionRelay;
71
+ // Use the caller's instructions when provided, else a sane default that
72
+ // guides Claude to use the reply tool (never silently empty).
73
+ this.instructions =
74
+ typeof instructions === 'string' && instructions.length > 0
75
+ ? instructions
76
+ : DEFAULT_INSTRUCTIONS;
77
+ this._replyHandler = null;
78
+ this._permissionRequestHandler = null;
79
+ const capabilities = {
80
+ tools: {},
81
+ experimental: {
82
+ 'claude/channel': {}
83
+ }
84
+ };
85
+ if (this.permissionRelay) {
86
+ capabilities.experimental['claude/channel/permission'] = {};
87
+ }
88
+ this.server = new Server({ name: name || 'mcp-channels', version: '0.1.0' }, { capabilities, instructions: this.instructions });
89
+ this._registerReplyTool();
90
+ if (this.permissionRelay) {
91
+ this._registerPermissionRelay();
92
+ }
93
+ }
94
+ /** Register the `reply` tool handlers (ListTools + CallTool). */
95
+ _registerReplyTool() {
96
+ this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
97
+ tools: [
98
+ {
99
+ name: 'reply',
100
+ description: 'Reply back to the origin of a channel event. Pass the event\'s ' +
101
+ 'reply_to token and your text; the framework routes it to the right ' +
102
+ 'backend (e.g. a GitHub/Jira comment).',
103
+ inputSchema: {
104
+ type: 'object',
105
+ properties: {
106
+ reply_to: {
107
+ type: 'string',
108
+ description: 'The opaque reply_to token from the inbound <channel> meta.'
109
+ },
110
+ text: { type: 'string', description: 'The reply text to post to origin.' }
111
+ },
112
+ required: ['reply_to', 'text']
113
+ }
114
+ }
115
+ ]
116
+ }));
117
+ this.server.setRequestHandler(CallToolRequestSchema, async (req) => {
118
+ const { name, arguments: args = {} } = req.params;
119
+ if (name !== 'reply') {
120
+ return { content: [{ type: 'text', text: `unknown tool: ${name}` }], isError: true };
121
+ }
122
+ if (!this._replyHandler) {
123
+ return {
124
+ content: [{ type: 'text', text: 'reply: no reply handler is configured' }],
125
+ isError: true
126
+ };
127
+ }
128
+ try {
129
+ const result = await this._replyHandler({
130
+ reply_to: args.reply_to,
131
+ text: args.text
132
+ });
133
+ if (!result || !result.ok) {
134
+ return {
135
+ content: [{ type: 'text', text: 'reply failed: could not deliver to origin' }],
136
+ isError: true
137
+ };
138
+ }
139
+ const ref = result.ref ? ` (${result.ref})` : '';
140
+ return { content: [{ type: 'text', text: `reply sent${ref}` }] };
141
+ }
142
+ catch (err) {
143
+ return {
144
+ content: [
145
+ { type: 'text', text: `reply failed: ${err?.message || String(err)}` }
146
+ ],
147
+ isError: true
148
+ };
149
+ }
150
+ });
151
+ }
152
+ /** Wire the inbound permission_request notification handler. */
153
+ _registerPermissionRelay() {
154
+ this.server.setNotificationHandler(z.object({
155
+ method: z.literal(PERMISSION_REQUEST_NOTIFICATION),
156
+ params: z
157
+ .object({
158
+ request_id: z.string(),
159
+ tool_name: z.string().optional(),
160
+ description: z.string().optional(),
161
+ input_preview: z.string().optional()
162
+ })
163
+ .loose()
164
+ }), async (notification) => {
165
+ if (this._permissionRequestHandler) {
166
+ await this._permissionRequestHandler(notification.params);
167
+ }
168
+ });
169
+ }
170
+ /**
171
+ * Set the handler invoked when Claude calls the `reply` tool.
172
+ */
173
+ setReplyHandler(fn) {
174
+ this._replyHandler = fn;
175
+ }
176
+ /**
177
+ * Set the handler invoked on an inbound permission_request (relay mode).
178
+ */
179
+ setPermissionRequestHandler(fn) {
180
+ this._permissionRequestHandler = fn;
181
+ }
182
+ /**
183
+ * Emit one channel event as a notifications/claude/channel.
184
+ */
185
+ async emit({ content, meta } = {}) {
186
+ await this.server.notification({
187
+ method: CHANNEL_NOTIFICATION,
188
+ params: {
189
+ content: content ?? '',
190
+ meta: sanitizeMeta(meta)
191
+ }
192
+ });
193
+ }
194
+ /**
195
+ * Emit a permission decision back to Claude (relay mode). `behavior` is coerced
196
+ * to EXACTLY 'allow' or 'deny' so the SPEC §2 contract holds regardless of what
197
+ * the caller passes — any value other than the literal 'allow' becomes 'deny'
198
+ * (silence/garbage means deny, the safe stance for an untrusted-input gate).
199
+ */
200
+ async emitPermission({ request_id, behavior }) {
201
+ await this.server.notification({
202
+ method: PERMISSION_NOTIFICATION,
203
+ params: { request_id, behavior: behavior === 'allow' ? 'allow' : 'deny' }
204
+ });
205
+ }
206
+ /**
207
+ * Connect the underlying MCP server to a transport.
208
+ */
209
+ async connect(transport) {
210
+ await this.server.connect(transport);
211
+ }
212
+ /** Close the underlying MCP server. */
213
+ async close() {
214
+ await this.server.close();
215
+ }
216
+ }
217
+ //# sourceMappingURL=server.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,96 @@
1
+ import type { RunOptions, ChannelEvent, SpawnConfig } from './types.js';
2
+ type AnySource = Record<string, any>;
3
+ /** The minimal structural seam the spawner needs from an adapters-like client.
4
+ * Typed locally (NOT imported from `@a5c-ai/adapters`) so the pure
5
+ * `buildSpawnRunOptions` mapping is duck-typed by `client.run()` and the package
6
+ * never couples to the SDK's exact internal RunOptions type. */
7
+ export interface AdaptersClientLike {
8
+ run(opts: RunOptions): unknown;
9
+ }
10
+ /**
11
+ * Normalize a configured agent id to the canonical adapter key, so a friendly
12
+ * alias (e.g. `claude-code`) resolves against the real adapters registry. An
13
+ * unknown/already-canonical id passes through unchanged.
14
+ */
15
+ export declare function normalizeAgentId(agent: string | null | undefined): string;
16
+ /**
17
+ * PURE mapping of (source, event, ctx) -> adapters `RunOptions` (SPEC §10.2).
18
+ * Reads the source's EFFECTIVE spawn config (`source.spawn`, already merged over
19
+ * the global defaults by config.js / the caller). Optional fields are OMITTED
20
+ * when unset (never emitted as `undefined`) so the adapter's own defaults apply.
21
+ */
22
+ export declare function buildSpawnRunOptions(source: AnySource & {
23
+ id: string;
24
+ spawn?: SpawnConfig;
25
+ baseDir?: string;
26
+ }, event: ChannelEvent, ctx?: {
27
+ configPath: string;
28
+ replySecret?: string;
29
+ resolveCliPath?: () => string;
30
+ }): RunOptions;
31
+ export interface SessionSpawnerDeps {
32
+ client?: AdaptersClientLike;
33
+ configPath: string;
34
+ replySecret?: string;
35
+ maxConcurrent?: number;
36
+ log?: (...args: unknown[]) => void;
37
+ resolveCliPath?: () => string;
38
+ loadClient?: () => Promise<AdaptersClientLike> | AdaptersClientLike;
39
+ onDispatch?: (source: AnySource, event: ChannelEvent) => void;
40
+ }
41
+ /**
42
+ * Launches sessions for surviving events via an injected adapters-like client.
43
+ * Bounded concurrency (a tiny promise-queue semaphore), error isolation (a
44
+ * throwing/rejecting `run` is caught + logged, never thrown to the caller), and
45
+ * an `onDispatch` hook so the poller records seen-on-dispatch exactly once.
46
+ */
47
+ export declare class SessionSpawner {
48
+ client: AdaptersClientLike | null;
49
+ configPath: string;
50
+ replySecret?: string;
51
+ maxConcurrent: number;
52
+ log: (...args: unknown[]) => void;
53
+ resolveCliPath?: () => string;
54
+ loadClient: () => Promise<AdaptersClientLike> | AdaptersClientLike;
55
+ onDispatch: ((source: AnySource, event: ChannelEvent) => void) | null;
56
+ _free: number;
57
+ _waiters: Array<() => void>;
58
+ _clientPromise: Promise<AdaptersClientLike> | null;
59
+ constructor({ client, configPath, replySecret, maxConcurrent, log, resolveCliPath, loadClient, onDispatch }?: SessionSpawnerDeps);
60
+ /**
61
+ * Resolve the adapters client: the injected one if present, else the lazily
62
+ * loaded one. A failure to obtain a client is surfaced as a CLEAR, actionable
63
+ * error (AC-25). Memoized so the dep is resolved at most once.
64
+ */
65
+ _getClient(): Promise<AdaptersClientLike>;
66
+ /**
67
+ * Startup validation hook (AC-25): if no client is injected, eagerly attempt
68
+ * to obtain one so a missing dep fails FAST and LOUDLY at startup rather than
69
+ * silently at event time. Throws a clear error when no client can be obtained.
70
+ * A no-op when a client is injected (stays offline).
71
+ */
72
+ validate(): Promise<void>;
73
+ /** Acquire a concurrency slot (awaits when the cap is reached). */
74
+ _acquire(): Promise<void>;
75
+ /** Release a concurrency slot, handing it to the next waiter if any. */
76
+ _release(): void;
77
+ /**
78
+ * Spawn a session for one surviving event. Builds RunOptions, resolves the
79
+ * client, acquires a concurrency slot, calls `client.run(opts)`, and returns
80
+ * the launch outcome. NEVER throws to the caller (the poller): a launch error
81
+ * is caught + logged and converted to `{ ok:false, error }`. The event is
82
+ * recorded as dispatched EXACTLY ONCE (AC-24), regardless of launch outcome.
83
+ *
84
+ * The missing-client/dep error (AC-25) is the one case allowed to surface as a
85
+ * rejection (it is a misconfiguration, not a per-launch failure).
86
+ */
87
+ spawn(source: AnySource & {
88
+ id: string;
89
+ spawn?: SpawnConfig;
90
+ }, event: ChannelEvent): Promise<{
91
+ ok: boolean;
92
+ handle?: unknown;
93
+ error?: unknown;
94
+ }>;
95
+ }
96
+ export {};
@@ -0,0 +1 @@
1
+ {"version":3,"file":"spawner.d.ts","sourceRoot":"","sources":["../src/spawner.ts"],"names":[],"mappings":"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"}