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 +7 -0
- package/README.md +8 -0
- package/dist/client.d.ts +5 -0
- package/dist/client.js +37 -10
- package/dist/otel.d.ts +54 -0
- package/dist/otel.js +179 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +12 -1
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
|
-
|
|
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
|
-
|
|
345
|
-
|
|
346
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
package/dist/version.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "macp-sdk-typescript",
|
|
3
|
-
"version": "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",
|