@sema-agent/connectors 0.3.0 → 0.4.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/CHANGELOG.md CHANGED
@@ -4,6 +4,53 @@ All notable changes to this package are documented here. Released sections are
4
4
  frozen: once a version is published its section is never edited again; new text
5
5
  goes into the next version's section.
6
6
 
7
+ ## 0.4.0 (2026-10-15)
8
+
9
+ ### Added
10
+
11
+ - `INTEGRATION.md` ships with the package: how a command-line tool mounts a
12
+ connector of the default catalog through `mount`, `mountCommand` and `seed`,
13
+ what it reads back, and what the engine then sees.
14
+ - The parked session (CN-046): in standalone mode the launcher never starts a
15
+ connector whose status would be `unauthorized` (no secret handle, the
16
+ status's own judgement); it answers the session itself, through the same
17
+ session and stages as a connector run. `initialize` is answered with the
18
+ requested protocol version, the tools and resources capabilities and
19
+ `serverInfo` `sema-connector` at this package's version; `tools/list` lists
20
+ no tools, `resources/list` the status entry alone, the status reads
21
+ `unauthorized` with no tools, and `ping` is answered. A tool call is refused
22
+ with `-32603`, another resource read with `-32602` and any other method with
23
+ `-32601`, each with `connector.auth_required` and the message
24
+ `connector <name> has no credentials; run its auth`; the refused call's
25
+ receipt is `refused` with `connector.auth_required`. The session ends with
26
+ exit 0 at the end of input or on a stop signal. So
27
+ `sema-connector-<name> status` before `auth` prints the `unauthorized`
28
+ status and exits 0, and an upstream program that would begin a login of its
29
+ own without a token never starts.
30
+
31
+ ### Changed
32
+
33
+ - A standalone launcher starts a connector only when it has a secret handle
34
+ (CN-046). Before, it started every connector, and one that refuses to start
35
+ without its handles (as the Gitea and GitHub connectors do) ended the run
36
+ with exit 5 and `connector.internal`. A connector that needs no credential
37
+ at all is parked as well until a secret handle is stored for it. A connector
38
+ whose secret handle has expired, and every hosted connector, is started as
39
+ before.
40
+
41
+ ### Fixed
42
+
43
+ - `sema-connector-<name> status` (CN-049) no longer discovers the status
44
+ resource's URI through `resources/list` and then insists on exactly one
45
+ entry there; it reads the status resource straight by its URI (the one
46
+ `statusUri` makes). Before, a started connector that lists resources of its
47
+ own made `resources/list` answer with more than the status entry the
48
+ launcher adds, so `status` always failed with "the launcher did not list
49
+ exactly one resource, its status" once authorized, even though the launcher
50
+ itself serves the status resource correctly. This affected the GitHub
51
+ connector once authorized (github-mcp-server lists 4 `ui://` resources of
52
+ its own); the Gitea connector, which lists none, was not affected.
53
+
7
54
  ## 0.3.0 (2026-10-15)
8
55
 
9
56
  ### Added
package/INTEGRATION.md ADDED
@@ -0,0 +1,68 @@
1
+ # Integrating `@sema-agent/connectors` from a command-line tool
2
+
3
+ How a tool outside this repository (the CLI's `connectors add`, for one) mounts a connector of the default
4
+ catalog into the engine's local configuration and reads what happened, using this package only. The rules
5
+ live in the repository's `docs/ARCHITECTURE.md` ("Mounting", "Seeding the default catalog", "Parked
6
+ session"); this page states the surface a tool consumes and does not restate the rules.
7
+
8
+ ## The configuration directory
9
+
10
+ One configuration directory per engine installation, the engine's local one: `CONFIG_LOCAL_DIR`, an
11
+ absolute path. `resolveConfigDir(process.env)` turns the variable into `{ ok: true, dir }` or
12
+ `{ ok: false, reason }` (not set, empty, or not absolute; the reason names the variable, never the value).
13
+ It is the same root `sema-connector seed --config-dir <dir>` seeds and the same root every connector CLI
14
+ (`sema-connector-<name> auth | mount | status`) reads from `CONFIG_LOCAL_DIR`. Entries land in
15
+ `<dir>/config.d/mcp.json`; a connector's secrets live in `<dir>/connectors/<name>/`.
16
+
17
+ ## Mounting one connector
18
+
19
+ ```ts
20
+ import { mount, mountCommand, resolveConfigDir, DEFAULT_CONNECTORS, SEED_VERSION, seed } from '@sema-agent/connectors'
21
+
22
+ const result = mount({ name: 'gitea', configDir: dir })
23
+ ```
24
+
25
+ - `mount(options: { name: string; configDir: string }): MountResult`. There is no `entry` argument: the entry
26
+ is fixed by the package and written as
27
+ `{"name": "<name>", "transport": {"kind": "stdio", "command": "sema-connector", "args": ["run", "<name>",
28
+ "--mode", "standalone", "--transport", "stdio", "--config-dir", "<configDir>"]}}`, never with environment
29
+ references or headers. A tool never composes an entry of its own.
30
+ - `MountResult` is `{ ok: true, changed: boolean, file: string }` (`changed: false` when the same entry was
31
+ already there, byte for byte apart from key order; `file` is the catalog file written) or
32
+ `{ ok: false, reason: MountRefusal, message: string }` with `reason` one of the closed set
33
+ `config_dir | invalid_name | reserved_name | unreadable | unexpected_shape | conflict | locked |
34
+ changed_during_write | write_failed`. `mount` does not throw for a refusal.
35
+ - Idempotence: a second `mount` of the same name changes nothing. Another entry under the same name (other
36
+ arguments, another `--config-dir`, `enabled: false`, environment references) is `conflict` and is never
37
+ overwritten; the tool reports it and leaves the person to remove the entry. `mount` keeps no marker of
38
+ its own.
39
+ - `mountCommand(name, configDir): number` is the same call with the CLI's output: on success one line on
40
+ standard output (`mounted connector <name> in <file>; restart the engine to apply`, or
41
+ `connector <name> is already mounted in <file>`) and exit 0; on a refusal one line on standard error,
42
+ `mount refused (<reason>): <message>`, and exit 1. A thin wrapper returns its number as its exit code.
43
+ - `mount` does not check that the connector package is installed: an entry for a connector that is not
44
+ installed makes the launcher exit 4 (`connector.not_installed`) when the engine starts it. A tool that
45
+ wants only installed connectors mounted uses the seed.
46
+
47
+ ## The default catalog and the seed
48
+
49
+ - `DEFAULT_CONNECTORS: readonly { name: string; since: number }[]`: the one list of default connectors, in
50
+ order; `name` is the connector (the package is `@sema-agent/connector-<name>`), `since` the seed version
51
+ that added it. `SEED_VERSION` is the newest `since`.
52
+ - `seed({ configDir }): SeedResult` mounts every installed connector of the default catalog through `mount`,
53
+ resolving each with the launcher's own rule first, and remembers the outcome per name in
54
+ `<dir>/config.d/.sema-connector-seed.json`, so an entry a person removed is never brought back. Readings per
55
+ name: `mounted | present | conflict | not_installed | refused`; the whole seed is refused as `config_dir |
56
+ locked | unreadable | unexpected_shape | write_failed`. `sema-connector seed --config-dir <dir>` is the same
57
+ call as a command (exit 0 when every connector is mounted, present or not installed, 1 otherwise, 2 for a bad
58
+ command line). A tool that offers "mount the defaults" runs the seed; a tool that offers "mount this one"
59
+ calls `mount`.
60
+
61
+ ## What the engine sees
62
+
63
+ Entries are read by the engine from `config.d/mcp.json` through the server's local file configuration
64
+ (the same path that reads every other stdio entry there); an entry `mount` wrote is read at the engine's next
65
+ start, and the launcher it names reads the connector's secrets under the same directory. Before `auth` has
66
+ stored the connector's credential the launcher parks the session: the connector lists no tools, its status
67
+ reads `unauthorized`, and a tool call is refused with `connector.auth_required`; after `auth`, the next start
68
+ runs the connector.
package/dist/cli-kit.js CHANGED
@@ -1,11 +1,11 @@
1
1
  import { spawn } from 'node:child_process';
2
- import { readFileSync } from 'node:fs';
3
- import { dirname, join } from 'node:path';
4
- import { fileURLToPath } from 'node:url';
5
2
  import { resolveConfigDir } from './config-dir.js';
6
3
  import { CONNECTOR_NAME } from './connector-name.js';
7
4
  import { hasRepeatedKey } from './json-text.js';
8
5
  import { mount } from './mount.js';
6
+ import { ownManifest } from './own-package.js';
7
+ import { MCP_PROTOCOL_VERSION } from './session.js';
8
+ import { statusUri } from './status.js';
9
9
  export function cliOutput(name) {
10
10
  if (typeof name !== 'string' || !CONNECTOR_NAME.test(name))
11
11
  throw new Error('a connector name is lowercase letters and digits in hyphen-separated words');
@@ -106,16 +106,10 @@ export function mountCommand(name, configDir) {
106
106
  say(r.changed ? `mounted connector ${name} in ${r.file}; restart the engine to apply` : `connector ${name} is already mounted in ${r.file}`);
107
107
  return 0;
108
108
  }
109
- const PROTOCOL_VERSION = '2025-11-25';
110
109
  const STATUS_TIMEOUT_MS = 10_000;
111
110
  const KILL_GRACE_MS = 2_000;
112
111
  const SHOWN_LINES = 5;
113
112
  const RECEIPT_PREFIX = 'sema-connector: receipt ';
114
- function ownManifest() {
115
- const url = new URL('../package.json', import.meta.url);
116
- const manifest = JSON.parse(readFileSync(url, 'utf8'));
117
- return { version: manifest.version, launcher: join(dirname(fileURLToPath(url)), manifest.bin['sema-connector'] ?? '') };
118
- }
119
113
  function lineReader(stream) {
120
114
  const lines = [];
121
115
  const waiters = [];
@@ -184,24 +178,17 @@ export async function statusCommand(name, configDir) {
184
178
  let found = null;
185
179
  let problem = null;
186
180
  try {
187
- const init = await reply(1, 'initialize', { protocolVersion: PROTOCOL_VERSION, capabilities: {}, clientInfo: { name: program, version: own.version } });
181
+ const init = await reply(1, 'initialize', { protocolVersion: MCP_PROTOCOL_VERSION, capabilities: {}, clientInfo: { name: program, version: own.version } });
188
182
  if (init?.result === undefined)
189
183
  problem = 'the launcher did not complete initialize';
190
184
  else {
191
185
  write({ method: 'notifications/initialized' });
192
- const list = await reply(2, 'resources/list');
193
- const entries = list?.result?.resources;
194
- const uri = Array.isArray(entries) && entries.length === 1 ? entries[0]?.uri : undefined;
195
- if (typeof uri !== 'string')
196
- problem = 'the launcher did not list exactly one resource, its status';
197
- else {
198
- const read = await reply(3, 'resources/read', { uri });
199
- const text = read?.result?.contents?.[0]?.text;
200
- if (typeof text !== 'string')
201
- problem = `the status could not be read${typeof read?.error?.message === 'string' ? `: ${read.error.message}` : ''}`;
202
- else
203
- found = JSON.parse(text);
204
- }
186
+ const read = await reply(2, 'resources/read', { uri: statusUri(name) });
187
+ const text = read?.result?.contents?.[0]?.text;
188
+ if (typeof text !== 'string')
189
+ problem = `the status could not be read${typeof read?.error?.message === 'string' ? `: ${read.error.message}` : ''}`;
190
+ else
191
+ found = JSON.parse(text);
205
192
  }
206
193
  }
207
194
  catch {
package/dist/launcher.js CHANGED
@@ -7,13 +7,14 @@ import { absoluteDir, given } from './config-dir.js';
7
7
  import { CONNECTOR_NAME } from './connector-name.js';
8
8
  import { serveHttp } from './http.js';
9
9
  import { keySetSource, obtainKeySet } from './jwks.js';
10
+ import { ownManifest } from './own-package.js';
10
11
  import { hostedResolver, standaloneResolver } from './principal.js';
11
12
  import { Scrubber, secretMatcher } from './redact.js';
12
13
  import { resolveConnector } from './resolve-connector.js';
13
14
  import { NO_HANDLES, copyForLaunch, handleDirectory } from './secrets.js';
14
15
  import { seed } from './seed.js';
15
- import { INSPECT_LIMIT_BYTES, Session } from './session.js';
16
- import { mintStatus } from './status.js';
16
+ import { INSPECT_LIMIT_BYTES, Session, parkedAnswer } from './session.js';
17
+ import { mintState, mintStatus } from './status.js';
17
18
  import { writeFailure } from './write-all.js';
18
19
  const EXIT = { ok: 0, notSeeded: 1, guard: 2, notInstalled: 4, connectorFailed: 5 };
19
20
  const NOT_INSTALLED = 'connector.not_installed';
@@ -381,31 +382,20 @@ function childEnvironment(env, connectorDir, secrets) {
381
382
  const hostedConnectorDir = (name) => join(tmpdir(), `sema-connector-${randomUUID()}`, 'connectors', name);
382
383
  const describe = (o) => `connector process ${o.stalled ? `stopped reading its input (stalled input: none of it taken for ${STALL_MS / 1000} s) and ` : ''}${o.error ? `could not be started (${o.error.message})` : o.signal !== null ? `was terminated by signal ${o.signal}` : `exited with code ${String(o.code)}`}`;
383
384
  const abnormal = (o) => o.code !== 0 || o.stalled === true;
384
- function startConnector(binPath, childEnv, session, scrubber, toUpstream, upstreamOut, log, stderrAfterExitMs) {
385
- const child = spawn(process.execPath, [binPath], { stdio: ['pipe', 'pipe', 'pipe'], detached: true, env: childEnv });
386
- const toGroup = (signal) => {
387
- if (child.pid === undefined)
388
- return false;
389
- try {
390
- process.kill(-child.pid, signal);
391
- return true;
392
- }
393
- catch {
394
- return false;
395
- }
396
- };
397
- const toChild = new Out(child.stdin);
398
- const perform = (actions) => {
385
+ function performer(log, toUpstream, toConnector) {
386
+ return (actions) => {
399
387
  for (const a of actions) {
400
388
  if (a.to === 'receipt')
401
389
  log.line(`${RECEIPT_PREFIX}${asciiJson(a.receipt)}\n`);
402
390
  else if (a.to === 'upstream')
403
391
  toUpstream(a.data);
404
392
  else
405
- toChild.write(a.data);
393
+ toConnector(a.data);
406
394
  }
407
395
  };
408
- const judged = (judge, unreadable) => {
396
+ }
397
+ function judging(perform) {
398
+ return (judge, unreadable) => {
409
399
  let actions;
410
400
  try {
411
401
  actions = judge();
@@ -415,6 +405,23 @@ function startConnector(binPath, childEnv, session, scrubber, toUpstream, upstre
415
405
  }
416
406
  perform(actions);
417
407
  };
408
+ }
409
+ function startConnector(binPath, childEnv, session, scrubber, toUpstream, upstreamOut, log, stderrAfterExitMs) {
410
+ const child = spawn(process.execPath, [binPath], { stdio: ['pipe', 'pipe', 'pipe'], detached: true, env: childEnv });
411
+ const toGroup = (signal) => {
412
+ if (child.pid === undefined)
413
+ return false;
414
+ try {
415
+ process.kill(-child.pid, signal);
416
+ return true;
417
+ }
418
+ catch {
419
+ return false;
420
+ }
421
+ };
422
+ const toChild = new Out(child.stdin);
423
+ const perform = performer(log, toUpstream, (data) => toChild.write(data));
424
+ const judged = judging(perform);
418
425
  let rung = 0;
419
426
  let stopped = false;
420
427
  const climb = (to, signal = 'SIGTERM') => {
@@ -458,7 +465,7 @@ function startConnector(binPath, childEnv, session, scrubber, toUpstream, upstre
458
465
  return ended;
459
466
  })();
460
467
  return {
461
- toChild,
468
+ input: toChild,
462
469
  perform,
463
470
  fromUpstream: (line, principal) => judged(() => session.fromUpstream(line, principal), () => session.upstreamOversized()),
464
471
  endInput: () => climb(1),
@@ -472,13 +479,37 @@ function startConnector(binPath, childEnv, session, scrubber, toUpstream, upstre
472
479
  outcome,
473
480
  };
474
481
  }
475
- async function runStdio(packageName, binPath, childEnv, session, scrubber, stdout, log) {
482
+ function parkedRun(session, connector, stdout, log) {
483
+ const serverInfo = { name: 'sema-connector', version: ownManifest().version };
484
+ let end = () => undefined;
485
+ const ended = new Promise((resolve) => (end = () => resolve({ code: 0, signal: null })));
486
+ const perform = performer(log, (data) => stdout.write(data), (data) => {
487
+ const answer = parkedAnswer(data, connector, serverInfo);
488
+ if (answer !== null)
489
+ judged(() => session.fromConnector(Buffer.from(JSON.stringify(answer), 'utf8')), () => session.connectorOversized());
490
+ });
491
+ const judged = judging(perform);
492
+ return {
493
+ input: stdout,
494
+ perform,
495
+ fromUpstream: (line, principal) => judged(() => session.fromUpstream(line, principal), () => session.upstreamOversized()),
496
+ endInput: end,
497
+ stop: () => {
498
+ end();
499
+ setTimeout(abandon, TEARDOWN_STEP_MS).unref();
500
+ },
501
+ outcome: ended.then((outcome) => {
502
+ perform(session.connectorGone('the launcher ended the parked session', false));
503
+ return outcome;
504
+ }),
505
+ };
506
+ }
507
+ async function runStdio(packageName, run, session, stdout, log) {
476
508
  const resolution = standaloneResolver().resolve({ headers: {} });
477
509
  if (!resolution.ok)
478
510
  throw new Error('the standalone principal cannot be refused');
479
511
  const anonymous = resolution.principal;
480
- const run = startConnector(binPath, childEnv, session, scrubber, (data) => stdout.write(data), stdout, log);
481
- const upstream = pump(process.stdin, run.toChild, { line: (line) => run.fromUpstream(line, anonymous), oversized: () => run.perform(session.upstreamOversized()) }, [stdout]);
512
+ const upstream = pump(process.stdin, run.input, { line: (line) => run.fromUpstream(line, anonymous), oversized: () => run.perform(session.upstreamOversized()) }, [stdout]);
482
513
  void upstream.then(() => run.endInput());
483
514
  for (const s of STOP_SIGNALS)
484
515
  process.on(s, (signal) => run.stop(signal));
@@ -500,7 +531,7 @@ async function runHosted(plan, keys, packageName, binPath, env, log) {
500
531
  const run = startConnector(binPath, childEnvironment(env, hostedConnectorDir(plan.name), NO_HANDLES), session, new Scrubber(NO_HANDLES.values), (data) => deliver(String(data)), null, log, TEARDOWN_STEP_MS);
501
532
  return {
502
533
  send: (raw, principal) => run.fromUpstream(raw, principal),
503
- busy: () => run.toChild.behind(),
534
+ busy: () => run.input.behind(),
504
535
  readStatus: (id, principal) => run.perform(session.statusFor(id, principal)),
505
536
  end: (signal) => (signal === undefined ? run.endInput() : run.stop(signal)),
506
537
  gone: run.outcome.then((o) => {
@@ -585,14 +616,19 @@ export async function main(argv, env) {
585
616
  log.line(`sema-connector: ${NOT_INSTALLED}: package ${resolved.packageName} ${resolved.reason}\n`);
586
617
  return finish(EXIT.notInstalled);
587
618
  }
619
+ const parked = mintState({ handles: secrets.handles, nowMs: Date.now() }) === 'unauthorized';
588
620
  const session = new Session({
589
621
  connector: plan.name,
590
622
  status: (tools) => mintStatus({ handles: secrets.handles, nowMs: Date.now(), tools }),
591
623
  ownIdPrefix: `sema-connector/${randomUUID()}`,
592
624
  withheld: secretMatcher(secrets.values),
625
+ ...(parked ? { parked: true } : {}),
593
626
  });
594
627
  try {
595
- return finish(await runStdio(resolved.packageName, resolved.binPath, childEnvironment(env, handleDirectory(plan.configDir, plan.name), secrets), session, new Scrubber(secrets.values), stdout, log));
628
+ const run = parked
629
+ ? parkedRun(session, plan.name, stdout, log)
630
+ : startConnector(resolved.binPath, childEnvironment(env, handleDirectory(plan.configDir, plan.name), secrets), session, new Scrubber(secrets.values), (data) => stdout.write(data), stdout, log);
631
+ return finish(await runStdio(resolved.packageName, run, session, stdout, log));
596
632
  }
597
633
  finally {
598
634
  secrets.remove();
@@ -0,0 +1,4 @@
1
+ export declare function ownManifest(): {
2
+ readonly version: string;
3
+ readonly launcher: string;
4
+ };
@@ -0,0 +1,8 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { dirname, join } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ export function ownManifest() {
5
+ const url = new URL('../package.json', import.meta.url);
6
+ const manifest = JSON.parse(readFileSync(url, 'utf8'));
7
+ return { version: manifest.version, launcher: join(dirname(fileURLToPath(url)), manifest.bin['sema-connector'] ?? '') };
8
+ }
package/dist/session.d.ts CHANGED
@@ -11,6 +11,7 @@ export type Action = {
11
11
  export declare const INSPECT_LIMIT_BYTES: number;
12
12
  export declare const INVALID_REQUEST = -32600;
13
13
  export declare const INTERNAL_ERROR = -32603;
14
+ export declare const MCP_PROTOCOL_VERSION = "2025-11-25";
14
15
  export declare const STATUS_BEFORE_INITIALIZE = "the status is not available before the connector has answered initialize";
15
16
  export type Message = Record<string, unknown>;
16
17
  export interface SessionOptions {
@@ -18,6 +19,7 @@ export interface SessionOptions {
18
19
  readonly status: (tools: readonly string[]) => ConnectorMintedStatus;
19
20
  readonly ownIdPrefix: string;
20
21
  readonly withheld: (line: Buffer, parsed: unknown) => boolean;
22
+ readonly parked?: true;
21
23
  }
22
24
  export declare const isId: (v: unknown) => v is JsonRpcId;
23
25
  export declare const idKey: (id: JsonRpcId) => string;
@@ -32,6 +34,10 @@ export type Reading = {
32
34
  };
33
35
  export declare function read(raw: Buffer): Reading;
34
36
  export declare const errorLine: (id: JsonRpcId | null, code: number, message: string, dataCode: ConnectorErrorCode) => string;
37
+ export declare function parkedAnswer(line: Buffer | string, connector: string, serverInfo: {
38
+ readonly name: string;
39
+ readonly version: string;
40
+ }): Message | null;
35
41
  export declare class Session {
36
42
  private readonly options;
37
43
  private readonly uri;
package/dist/session.js CHANGED
@@ -8,15 +8,19 @@ const LIMIT_TEXT = `${INSPECT_LIMIT_BYTES / (1024 * 1024)} MiB`;
8
8
  const MAX_STATUS_PAGES = 64;
9
9
  const FORBIDDEN_SCOPE = 'connector.forbidden_scope';
10
10
  const INTERNAL = 'connector.internal';
11
+ const AUTH_REQUIRED = 'connector.auth_required';
11
12
  export const INVALID_REQUEST = -32600;
13
+ const METHOD_NOT_FOUND = -32601;
12
14
  const INVALID_PARAMS = -32602;
13
15
  export const INTERNAL_ERROR = -32603;
16
+ export const MCP_PROTOCOL_VERSION = '2025-11-25';
14
17
  const UNREADABLE_REQUEST = `the launcher forwards only single JSON-RPC messages of at most ${LIMIT_TEXT}, on one line and without repeated keys, which it can check against the granted scope`;
15
18
  const WITHHELD = 'the connector wrote one of its secret values into this reply, so the launcher withheld it';
16
19
  const NO_ID = 'the launcher forwards a request only with a string or number id, and needs one on the requests whose replies it rewrites or answers';
17
20
  export const STATUS_BEFORE_INITIALIZE = 'the status is not available before the connector has answered initialize';
18
21
  const TAKEN_ID = 'the connector may still answer an earlier request with this id (it has not answered it yet, or the launcher answered it after a line it could not read); use another id';
19
22
  const UNREADABLE_REPLY = `the connector wrote a message the launcher cannot read (over ${LIMIT_TEXT}, a batch, or a repeated key), which may have been this reply`;
23
+ const noCredentials = (connector) => `connector ${connector} has no credentials; run its auth`;
20
24
  const ERROR_CODES = new Set(Object.keys({
21
25
  'connector.auth_required': 0,
22
26
  'connector.auth_expired': 0,
@@ -49,7 +53,31 @@ export function read(raw) {
49
53
  return hasRepeatedKey(text) ? { kind: 'unreadable' } : { kind: 'message', m };
50
54
  }
51
55
  const lineOf = (m) => JSON.stringify(m) + '\n';
52
- export const errorLine = (id, code, message, dataCode) => lineOf({ jsonrpc: '2.0', id, error: { code, message, data: { code: dataCode } } });
56
+ const errorOf = (id, code, message, dataCode) => ({ jsonrpc: '2.0', id, error: { code, message, data: { code: dataCode } } });
57
+ export const errorLine = (id, code, message, dataCode) => lineOf(errorOf(id, code, message, dataCode));
58
+ export function parkedAnswer(line, connector, serverInfo) {
59
+ const r = read(typeof line === 'string' ? Buffer.from(line, 'utf8') : line);
60
+ if (r.kind !== 'message' || typeof r.m.method !== 'string' || !isId(r.m.id))
61
+ return null;
62
+ const id = r.m.id;
63
+ const result = (value) => ({ jsonrpc: '2.0', id, result: value });
64
+ switch (r.m.method) {
65
+ case 'initialize': {
66
+ const requested = asObject(r.m.params)?.protocolVersion;
67
+ return result({ protocolVersion: typeof requested === 'string' ? requested : MCP_PROTOCOL_VERSION, capabilities: { tools: {} }, serverInfo: { ...serverInfo } });
68
+ }
69
+ case 'ping':
70
+ return result({});
71
+ case 'tools/list':
72
+ return result({ tools: [] });
73
+ case 'resources/list':
74
+ return result({ resources: [] });
75
+ case 'resources/read':
76
+ return errorOf(id, INVALID_PARAMS, noCredentials(connector), AUTH_REQUIRED);
77
+ default:
78
+ return errorOf(id, METHOD_NOT_FOUND, noCredentials(connector), AUTH_REQUIRED);
79
+ }
80
+ }
53
81
  const up = (data) => ({ to: 'upstream', data });
54
82
  const down = (data) => ({ to: 'connector', data });
55
83
  const same = (raw) => Buffer.concat([raw, NEWLINE]);
@@ -91,6 +119,8 @@ export class Session {
91
119
  case 'tools/call': {
92
120
  if (typeof name !== 'string' || !isGranted(grant, name))
93
121
  return [up(errorLine(id, INVALID_PARAMS, 'the requested tool is not in the granted scope', FORBIDDEN_SCOPE)), ...this.settle(call, 'refused', FORBIDDEN_SCOPE)];
122
+ if (this.options.parked === true)
123
+ return id === null ? [] : [up(errorLine(id, INTERNAL_ERROR, noCredentials(this.options.connector), AUTH_REQUIRED)), ...this.settle(call, 'refused', AUTH_REQUIRED)];
94
124
  return this.forward(raw, id, grant, undefined, undefined, call);
95
125
  }
96
126
  case 'resources/read':
package/dist/status.d.ts CHANGED
@@ -1,9 +1,13 @@
1
- import type { ConnectorMintedStatus } from '@sema-agent/connectors-contract';
1
+ import type { ConnectorMintedState, ConnectorMintedStatus } from '@sema-agent/connectors-contract';
2
2
  import { type SecretHandle } from './secrets.js';
3
3
  export declare const STATUS_MIME_TYPE = "application/json";
4
4
  export declare const AUTHORIZE_URL_PATTERN: RegExp;
5
5
  export declare function statusUri(connector: string): string;
6
6
  export declare function statusResource(connector: string): Record<string, string>;
7
+ export declare function mintState(input: {
8
+ readonly handles: readonly SecretHandle[];
9
+ readonly nowMs: number;
10
+ }): ConnectorMintedState;
7
11
  export declare function mintStatus(input: {
8
12
  readonly handles: readonly SecretHandle[];
9
13
  readonly nowMs: number;
package/dist/status.js CHANGED
@@ -13,9 +13,12 @@ export function statusResource(connector) {
13
13
  mimeType: STATUS_MIME_TYPE,
14
14
  };
15
15
  }
16
- export function mintStatus(input) {
16
+ export function mintState(input) {
17
17
  const secrets = input.handles.filter(isSecret);
18
- const state = secrets.length === 0 ? 'unauthorized' : secrets.some((h) => h.expiresAt !== undefined && Date.parse(h.expiresAt) <= input.nowMs) ? 'expired' : 'authorized';
18
+ return secrets.length === 0 ? 'unauthorized' : secrets.some((h) => h.expiresAt !== undefined && Date.parse(h.expiresAt) <= input.nowMs) ? 'expired' : 'authorized';
19
+ }
20
+ export function mintStatus(input) {
21
+ const state = mintState(input);
19
22
  const link = state === 'unauthorized' && input.authorizeUrl !== undefined && AUTHORIZE_URL_PATTERN.test(input.authorizeUrl) ? { authorizeUrl: input.authorizeUrl } : {};
20
23
  return { state, tools: [...input.tools], ...link };
21
24
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sema-agent/connectors",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Shared connector layer and the sema-connector launcher (MCP stdio proxy with principal, scope, normalize, status and trace stages).",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -19,7 +19,8 @@
19
19
  "files": [
20
20
  "dist",
21
21
  "bin",
22
- "CHANGELOG.md"
22
+ "CHANGELOG.md",
23
+ "INTEGRATION.md"
23
24
  ],
24
25
  "scripts": {
25
26
  "build": "tsc -p tsconfig.json"