macp-sdk-typescript 0.20.0 → 0.21.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,13 @@ All notable changes to `macp-sdk-typescript` are documented here. The format
4
4
  follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the
5
5
  project uses [Semantic Versioning](https://semver.org/).
6
6
 
7
+ ## [0.21.0](https://github.com/multiagentcoordinationprotocol/macp-sdk-typescript/compare/v0.20.0...v0.21.0) (2026-10-26)
8
+
9
+
10
+ ### Features
11
+
12
+ * optional OpenTelemetry send spans and W3C trace propagation (W5 S2) ([#212](https://github.com/multiagentcoordinationprotocol/macp-sdk-typescript/issues/212)) ([28ce7a1](https://github.com/multiagentcoordinationprotocol/macp-sdk-typescript/commit/28ce7a149e6803f5828416eafbc73e4067f80cd7))
13
+
7
14
  ## [0.20.0](https://github.com/multiagentcoordinationprotocol/macp-sdk-typescript/compare/v0.19.0...v0.20.0) (2026-10-26)
8
15
 
9
16
 
package/README.md CHANGED
@@ -492,6 +492,14 @@ If you hold the snake_case §3 projection (a receipt's `commitment`) instead of
492
492
 
493
493
  The `macp-sdk-typescript/receipt` subpath is browser-safe: it pulls in no gRPC, no `node:crypto` and no `Buffer`. The same functions are also exported from the package root. The tests replay the spec's `schemas/conformance/receipt/` vectors and rebuild the spec's example receipt byte for byte.
494
494
 
495
+ ## OpenTelemetry
496
+
497
+ Install the optional peer dependency `@opentelemetry/api` and register a tracer provider. The SDK then:
498
+ - emits one CLIENT span per `send()`, named `send {macp.mode.id}`, with the MACP semantic-convention attributes;
499
+ - puts W3C `traceparent`/`tracestate` in the gRPC metadata of every call made while a valid span context is active.
500
+
501
+ It never records payloads, `reason` text or error messages. Without the package, nothing is loaded or emitted. See [docs/guides/observability.md](docs/guides/observability.md) for the attribute set, the outcome rules and the limitations.
502
+
495
503
  ## Error Handling
496
504
 
497
505
  ```typescript
package/dist/client.d.ts CHANGED
@@ -115,6 +115,11 @@ export declare class MacpClient {
115
115
  * that one in-flight call (and the first caller's deadline).
116
116
  */
117
117
  private requireCapability;
118
+ /**
119
+ * The single metadata choke point: auth, plus W3C trace context
120
+ * (RFC-MACP-0006 §2.1) when `@opentelemetry/api` is installed and a valid
121
+ * span context is active — or, for Send, the send span's own context.
122
+ */
118
123
  private metadata;
119
124
  private deadline;
120
125
  private unary;
package/dist/client.js CHANGED
@@ -47,6 +47,7 @@ const path = __importStar(require("node:path"));
47
47
  const grpc = __importStar(require("@grpc/grpc-js"));
48
48
  const protoLoader = __importStar(require("@grpc/proto-loader"));
49
49
  const auth_1 = require("./auth");
50
+ const otel_1 = require("./otel");
50
51
  const constants_1 = require("./constants");
51
52
  const envelope_1 = require("./envelope");
52
53
  const errors_1 = require("./errors");
@@ -339,17 +340,27 @@ class MacpClient {
339
340
  `${rpc} is optional and a client MUST NOT call it otherwise (${rfc})`);
340
341
  }
341
342
  }
342
- metadata(auth) {
343
+ /**
344
+ * The single metadata choke point: auth, plus W3C trace context
345
+ * (RFC-MACP-0006 §2.1) when `@opentelemetry/api` is installed and a valid
346
+ * span context is active — or, for Send, the send span's own context.
347
+ */
348
+ metadata(auth, traceContext) {
343
349
  const selected = auth ?? this.auth;
344
- if (!selected)
345
- return undefined;
346
- return (0, auth_1.metadataFromAuth)(selected);
350
+ const metadata = selected ? (0, auth_1.metadataFromAuth)(selected) : undefined;
351
+ const headers = (0, otel_1.traceHeaders)(traceContext);
352
+ if (headers.length === 0)
353
+ return metadata;
354
+ const withTrace = metadata ?? new grpc.Metadata();
355
+ for (const [key, value] of headers)
356
+ withTrace.set(key, value);
357
+ return withTrace;
347
358
  }
348
359
  deadline(deadlineMs) {
349
360
  const resolved = deadlineMs ?? this.defaultDeadlineMs;
350
361
  return resolved ? new Date(Date.now() + resolved) : undefined;
351
362
  }
352
- unary(method, request, auth, deadlineMs, ackStatuses, registryCodeAsAck = false) {
363
+ unary(method, request, auth, deadlineMs, ackStatuses, registryCodeAsAck = false, traceContext) {
353
364
  return new Promise((resolve, reject) => {
354
365
  const callback = (error, response) => {
355
366
  if (error) {
@@ -373,7 +384,7 @@ class MacpClient {
373
384
  }
374
385
  };
375
386
  const deadline = this.deadline(deadlineMs);
376
- const metadata = this.metadata(auth);
387
+ const metadata = this.metadata(auth, traceContext);
377
388
  if (metadata && deadline)
378
389
  this.client[method](request, metadata, { deadline }, callback);
379
390
  else if (metadata)
@@ -410,14 +421,30 @@ class MacpClient {
410
421
  }
411
422
  async send(envelope, options) {
412
423
  const auth = this.requireAuth(options?.auth);
413
- const response = await this.unary('Send', { envelope }, auth, options?.deadlineMs,
424
+ // A malformed credential fails here, before the span starts: a send that
425
+ // never reaches the wire emits no span.
426
+ (0, auth_1.validateAuth)(auth);
427
+ // One CLIENT span per call (undefined without @opentelemetry/api); its
428
+ // own context goes into the Send metadata so the runtime's server span
429
+ // is its child.
430
+ const span = (0, otel_1.startSendSpan)(envelope);
431
+ const call = () => this.unary('Send', { envelope }, auth, options?.deadlineMs,
414
432
  // gRPC ALREADY_EXISTS/FAILED_PRECONDITION on Send are application-level
415
433
  // rejections the runtime couldn't express as a normal NACK (issue #107);
416
434
  // surface them as MacpAckError, matching macp-sdk-python (client.py:442-452).
417
435
  // That status map is the pre-0.9.0 fallback: a `macp-error-code` key
418
436
  // (runtime ≥ 0.9.0) supplies the exact registry code instead.
419
- { ALREADY_EXISTS: constants_1.SESSION_ALREADY_EXISTS, FAILED_PRECONDITION: constants_1.POLICY_DENIED }, true);
420
- const ack = response.ack;
437
+ { ALREADY_EXISTS: constants_1.SESSION_ALREADY_EXISTS, FAILED_PRECONDITION: constants_1.POLICY_DENIED }, true, span?.context);
438
+ let response;
439
+ try {
440
+ response = await (span ? span.run(call) : call());
441
+ }
442
+ catch (error) {
443
+ span?.end({ error });
444
+ throw error;
445
+ }
446
+ const ack = response?.ack;
447
+ span?.end({ ack: ack ?? {} });
421
448
  // Duplicate acks are success — the message was already accepted
422
449
  if (ack?.duplicate)
423
450
  return ack;
@@ -683,7 +710,7 @@ class MacpClient {
683
710
  */
684
711
  watchSignals(auth) {
685
712
  const selected = this.requireAuth(auth);
686
- const metadata = (0, auth_1.metadataFromAuth)(selected);
713
+ const metadata = this.metadata(selected);
687
714
  return this.client.WatchSignals({}, metadata);
688
715
  }
689
716
  async sendSignal(options) {
package/dist/otel.d.ts ADDED
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Optional OpenTelemetry instrumentation (docs/opentelemetry.md in the spec
3
+ * repo, semconv `span.macp.send`; propagation per RFC-MACP-0006 §2.1).
4
+ *
5
+ * `@opentelemetry/api` is an optional peer dependency. It is required lazily,
6
+ * on the first RPC, never at import, so loading the SDK never touches it, and
7
+ * when it is not installed every function here is a no-op. Nothing in this
8
+ * module may throw into the send path: instrumentation must never fail a call.
9
+ *
10
+ * Internal: not exported from `src/index.ts`, and never imported by the
11
+ * browser-safe `./receipt` subpath.
12
+ */
13
+ import type * as OtelApiModule from '@opentelemetry/api';
14
+ import type { Ack, Envelope } from './types';
15
+ type OtelApi = typeof OtelApiModule;
16
+ /** A span context, opaque outside this module. */
17
+ export type TraceContext = OtelApiModule.SpanContext;
18
+ /**
19
+ * Test seam: `null` forces "not installed", a module object replaces the real
20
+ * one, `undefined` restores normal loading.
21
+ */
22
+ export declare function setOtelApiForTesting(api: OtelApi | null | undefined): void;
23
+ /**
24
+ * W3C `traceparent`/`tracestate` for the given span context, or the active
25
+ * one when none is given. Built from the API alone rather than through the
26
+ * global propagator, which would also inject `baggage` (arbitrary user data)
27
+ * and follow a user's B3/Jaeger configuration; §2.1 names only these two.
28
+ */
29
+ export declare function traceHeaders(explicit?: TraceContext): Array<[string, string]>;
30
+ /** One in-flight `send {macp.mode.id}` CLIENT span. */
31
+ export interface SendSpan {
32
+ /** The span's own context, injected into the Send metadata. */
33
+ readonly context: TraceContext | undefined;
34
+ /**
35
+ * Run `fn` with this span active, so a transport client span (for example
36
+ * from gRPC auto-instrumentation) is its child, as the semconv note on
37
+ * `span.macp.send` requires. Calls `fn` exactly once; an OpenTelemetry
38
+ * failure falls back to calling it directly.
39
+ */
40
+ run<T>(fn: () => T): T;
41
+ /** Record the outcome and end the span. Safe to call once; never throws. */
42
+ end(result: {
43
+ ack: Ack;
44
+ } | {
45
+ error: unknown;
46
+ }): void;
47
+ }
48
+ /**
49
+ * Start the CLIENT span for one `send()`, or return `undefined` when
50
+ * OpenTelemetry is not installed (or the tracer fails). Only identifiers and
51
+ * types are recorded — never payload, `reason`, or any error message.
52
+ */
53
+ export declare function startSendSpan(envelope: Envelope): SendSpan | undefined;
54
+ export {};
package/dist/otel.js ADDED
@@ -0,0 +1,179 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.setOtelApiForTesting = setOtelApiForTesting;
4
+ exports.traceHeaders = traceHeaders;
5
+ exports.startSendSpan = startSendSpan;
6
+ const errors_1 = require("./errors");
7
+ const TRACER_NAME = 'macp-sdk-typescript';
8
+ /** OpenTelemetry's fallback `error.type` when no better value is known. */
9
+ const ERROR_TYPE_OTHER = '_OTHER';
10
+ // undefined: not tried yet; null: tried, absent.
11
+ let loaded;
12
+ let override;
13
+ let tracerVersion;
14
+ /**
15
+ * Test seam: `null` forces "not installed", a module object replaces the real
16
+ * one, `undefined` restores normal loading.
17
+ */
18
+ function setOtelApiForTesting(api) {
19
+ override = api;
20
+ }
21
+ function otelApi() {
22
+ if (override !== undefined)
23
+ return override ?? undefined;
24
+ if (loaded === undefined) {
25
+ try {
26
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
27
+ loaded = require('@opentelemetry/api');
28
+ }
29
+ catch {
30
+ loaded = null;
31
+ }
32
+ }
33
+ return loaded ?? undefined;
34
+ }
35
+ function sdkVersion() {
36
+ if (tracerVersion === undefined) {
37
+ try {
38
+ // Resolves to the package root from both src/ and dist/.
39
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
40
+ tracerVersion = String(require('../package.json').version ?? '');
41
+ }
42
+ catch {
43
+ tracerVersion = '';
44
+ }
45
+ }
46
+ return tracerVersion || undefined;
47
+ }
48
+ /**
49
+ * W3C `traceparent`/`tracestate` for the given span context, or the active
50
+ * one when none is given. Built from the API alone rather than through the
51
+ * global propagator, which would also inject `baggage` (arbitrary user data)
52
+ * and follow a user's B3/Jaeger configuration; §2.1 names only these two.
53
+ */
54
+ function traceHeaders(explicit) {
55
+ const api = otelApi();
56
+ if (!api)
57
+ return [];
58
+ try {
59
+ const spanContext = explicit ?? api.trace.getSpanContext(api.context.active());
60
+ if (!spanContext || !api.isSpanContextValid(spanContext))
61
+ return [];
62
+ const flags = (spanContext.traceFlags & 0xff).toString(16).padStart(2, '0');
63
+ const headers = [
64
+ ['traceparent', `00-${spanContext.traceId}-${spanContext.spanId}-${flags}`],
65
+ ];
66
+ const tracestate = spanContext.traceState?.serialize();
67
+ if (tracestate)
68
+ headers.push(['tracestate', tracestate]);
69
+ return headers;
70
+ }
71
+ catch {
72
+ return [];
73
+ }
74
+ }
75
+ /**
76
+ * Start the CLIENT span for one `send()`, or return `undefined` when
77
+ * OpenTelemetry is not installed (or the tracer fails). Only identifiers and
78
+ * types are recorded — never payload, `reason`, or any error message.
79
+ */
80
+ function startSendSpan(envelope) {
81
+ const api = otelApi();
82
+ if (!api)
83
+ return undefined;
84
+ try {
85
+ const attributes = {
86
+ 'macp.operation.name': 'send',
87
+ 'macp.message.id': envelope.messageId,
88
+ 'macp.message.type': envelope.messageType,
89
+ };
90
+ if (envelope.macpVersion)
91
+ attributes['macp.version'] = envelope.macpVersion;
92
+ if (envelope.sessionId)
93
+ attributes['macp.session.id'] = envelope.sessionId;
94
+ if (envelope.mode)
95
+ attributes['macp.mode.id'] = envelope.mode;
96
+ if (envelope.sender)
97
+ attributes['macp.sender.id'] = envelope.sender;
98
+ const name = envelope.mode ? `send ${envelope.mode}` : 'send';
99
+ const span = api.trace
100
+ .getTracer(TRACER_NAME, sdkVersion())
101
+ .startSpan(name, { kind: api.SpanKind.CLIENT, attributes }, api.context.active());
102
+ let ended = false;
103
+ return {
104
+ context: safe(() => span.spanContext()),
105
+ run(fn) {
106
+ let called = false;
107
+ let returned = false;
108
+ let result;
109
+ const once = () => {
110
+ called = true;
111
+ result = fn();
112
+ returned = true;
113
+ return result;
114
+ };
115
+ try {
116
+ return api.context.with(api.trace.setSpan(api.context.active(), span), once);
117
+ }
118
+ catch (error) {
119
+ // fn's own throw propagates; an OpenTelemetry failure before or
120
+ // after fn never replaces the call's real result.
121
+ if (!called)
122
+ return fn();
123
+ if (returned)
124
+ return result;
125
+ throw error;
126
+ }
127
+ },
128
+ end(result) {
129
+ if (ended)
130
+ return;
131
+ ended = true;
132
+ safe(() => {
133
+ recordOutcome(api, span, result);
134
+ });
135
+ safe(() => span.end());
136
+ },
137
+ };
138
+ }
139
+ catch {
140
+ return undefined;
141
+ }
142
+ }
143
+ function recordOutcome(api, span, result) {
144
+ if ('ack' in result) {
145
+ const { ack } = result;
146
+ if (ack.duplicate)
147
+ span.setAttribute('macp.envelope.outcome', 'duplicate');
148
+ else if (ack.ok)
149
+ span.setAttribute('macp.envelope.outcome', 'accepted');
150
+ else
151
+ rejected(span, ack.error?.code);
152
+ return;
153
+ }
154
+ const { error } = result;
155
+ // A rejection is a normal protocol outcome: status stays UNSET. The SDK
156
+ // raises MacpAckError for a NACK, a status carrying `macp-error-code`, and
157
+ // the pre-0.9 status fallback — the span classifies exactly as the caller's
158
+ // exception does.
159
+ if (error instanceof errors_1.MacpAckError) {
160
+ rejected(span, error.failure.code);
161
+ return;
162
+ }
163
+ const errorType = (error instanceof errors_1.MacpTransportError ? (error.macpErrorCode ?? error.code) : undefined) || ERROR_TYPE_OTHER;
164
+ span.setAttribute('error.type', errorType);
165
+ // The description is the code only; exception messages are never recorded.
166
+ span.setStatus({ code: api.SpanStatusCode.ERROR, message: errorType });
167
+ }
168
+ function rejected(span, code) {
169
+ span.setAttribute('macp.envelope.outcome', 'rejected');
170
+ span.setAttribute('error.type', code || ERROR_TYPE_OTHER);
171
+ }
172
+ function safe(fn) {
173
+ try {
174
+ return fn();
175
+ }
176
+ catch {
177
+ return undefined;
178
+ }
179
+ }
package/dist/version.d.ts CHANGED
@@ -4,4 +4,4 @@
4
4
  * Kept in sync with package.json by release-please (see the
5
5
  * `x-release-please-version` annotation).
6
6
  */
7
- export declare const VERSION = "0.20.0";
7
+ export declare const VERSION = "0.21.0";
package/dist/version.js CHANGED
@@ -7,4 +7,4 @@ exports.VERSION = void 0;
7
7
  * Kept in sync with package.json by release-please (see the
8
8
  * `x-release-please-version` annotation).
9
9
  */
10
- exports.VERSION = '0.20.0'; // x-release-please-version
10
+ exports.VERSION = '0.21.0'; // x-release-please-version
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "macp-sdk-typescript",
3
- "version": "0.20.0",
3
+ "version": "0.21.0",
4
4
  "description": "TypeScript SDK for the MACP Rust runtime",
5
5
  "license": "Apache-2.0",
6
6
  "type": "commonjs",
@@ -65,7 +65,18 @@
65
65
  "@noble/hashes": "^1.8.0",
66
66
  "protobufjs": "^8.6.5"
67
67
  },
68
+ "peerDependencies": {
69
+ "@opentelemetry/api": ">=1.3.0 <2"
70
+ },
71
+ "peerDependenciesMeta": {
72
+ "@opentelemetry/api": {
73
+ "optional": true
74
+ }
75
+ },
68
76
  "devDependencies": {
77
+ "@opentelemetry/api": "~1.9.1",
78
+ "@opentelemetry/context-async-hooks": "^2.12.0",
79
+ "@opentelemetry/sdk-trace-base": "^2.12.0",
69
80
  "@types/node": "^26.4.0",
70
81
  "@typescript-eslint/eslint-plugin": "^8.68.0",
71
82
  "@typescript-eslint/parser": "^8.68.0",