toolcraft 0.0.119 → 0.0.121
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/composition.json +17 -2
- package/dist/composition.json +17 -2
- package/dist/http.compile-check.d.ts +1 -0
- package/dist/http.compile-check.js +31 -0
- package/dist/http.d.ts +19 -0
- package/dist/http.js +62 -0
- package/dist/mcp.js +26 -11
- package/node_modules/jose/LICENSE.md +21 -0
- package/node_modules/jose/README.md +153 -0
- package/node_modules/jose/dist/types/index.d.ts +55 -0
- package/node_modules/jose/dist/types/jwe/compact/decrypt.d.ts +31 -0
- package/node_modules/jose/dist/types/jwe/compact/encrypt.d.ts +65 -0
- package/node_modules/jose/dist/types/jwe/flattened/decrypt.d.ts +31 -0
- package/node_modules/jose/dist/types/jwe/flattened/encrypt.d.ts +83 -0
- package/node_modules/jose/dist/types/jwe/general/decrypt.d.ts +38 -0
- package/node_modules/jose/dist/types/jwe/general/encrypt.d.ts +74 -0
- package/node_modules/jose/dist/types/jwk/embedded.d.ts +17 -0
- package/node_modules/jose/dist/types/jwk/thumbprint.d.ts +32 -0
- package/node_modules/jose/dist/types/jwks/local.d.ts +29 -0
- package/node_modules/jose/dist/types/jwks/remote.d.ts +237 -0
- package/node_modules/jose/dist/types/jws/compact/sign.d.ts +36 -0
- package/node_modules/jose/dist/types/jws/compact/verify.d.ts +33 -0
- package/node_modules/jose/dist/types/jws/flattened/sign.d.ts +42 -0
- package/node_modules/jose/dist/types/jws/flattened/verify.d.ts +33 -0
- package/node_modules/jose/dist/types/jws/general/sign.d.ts +53 -0
- package/node_modules/jose/dist/types/jws/general/verify.d.ts +41 -0
- package/node_modules/jose/dist/types/jwt/decrypt.d.ts +35 -0
- package/node_modules/jose/dist/types/jwt/encrypt.d.ts +91 -0
- package/node_modules/jose/dist/types/jwt/sign.d.ts +43 -0
- package/node_modules/jose/dist/types/jwt/unsecured.d.ts +43 -0
- package/node_modules/jose/dist/types/jwt/verify.d.ts +37 -0
- package/node_modules/jose/dist/types/key/export.d.ts +33 -0
- package/node_modules/jose/dist/types/key/generate_key_pair.d.ts +47 -0
- package/node_modules/jose/dist/types/key/generate_secret.d.ts +35 -0
- package/node_modules/jose/dist/types/key/import.d.ts +83 -0
- package/node_modules/jose/dist/types/types.d.ts +852 -0
- package/node_modules/jose/dist/types/util/base64url.d.ts +9 -0
- package/node_modules/jose/dist/types/util/decode_jwt.d.ts +18 -0
- package/node_modules/jose/dist/types/util/decode_protected_header.d.ts +17 -0
- package/node_modules/jose/dist/types/util/errors.d.ts +213 -0
- package/node_modules/jose/dist/webapi/index.js +32 -0
- package/node_modules/jose/dist/webapi/jwe/compact/decrypt.js +27 -0
- package/node_modules/jose/dist/webapi/jwe/compact/encrypt.js +27 -0
- package/node_modules/jose/dist/webapi/jwe/flattened/decrypt.js +159 -0
- package/node_modules/jose/dist/webapi/jwe/flattened/encrypt.js +167 -0
- package/node_modules/jose/dist/webapi/jwe/general/decrypt.js +31 -0
- package/node_modules/jose/dist/webapi/jwe/general/encrypt.js +182 -0
- package/node_modules/jose/dist/webapi/jwk/embedded.js +17 -0
- package/node_modules/jose/dist/webapi/jwk/thumbprint.js +68 -0
- package/node_modules/jose/dist/webapi/jwks/local.js +119 -0
- package/node_modules/jose/dist/webapi/jwks/remote.js +179 -0
- package/node_modules/jose/dist/webapi/jws/compact/sign.js +18 -0
- package/node_modules/jose/dist/webapi/jws/compact/verify.js +21 -0
- package/node_modules/jose/dist/webapi/jws/flattened/sign.js +89 -0
- package/node_modules/jose/dist/webapi/jws/flattened/verify.js +110 -0
- package/node_modules/jose/dist/webapi/jws/general/sign.js +70 -0
- package/node_modules/jose/dist/webapi/jws/general/verify.js +24 -0
- package/node_modules/jose/dist/webapi/jwt/decrypt.js +23 -0
- package/node_modules/jose/dist/webapi/jwt/encrypt.js +101 -0
- package/node_modules/jose/dist/webapi/jwt/sign.js +52 -0
- package/node_modules/jose/dist/webapi/jwt/unsecured.js +63 -0
- package/node_modules/jose/dist/webapi/jwt/verify.js +15 -0
- package/node_modules/jose/dist/webapi/key/export.js +11 -0
- package/node_modules/jose/dist/webapi/key/generate_key_pair.js +97 -0
- package/node_modules/jose/dist/webapi/key/generate_secret.js +40 -0
- package/node_modules/jose/dist/webapi/key/import.js +57 -0
- package/node_modules/jose/dist/webapi/lib/aesgcmkw.js +15 -0
- package/node_modules/jose/dist/webapi/lib/aeskw.js +25 -0
- package/node_modules/jose/dist/webapi/lib/asn1.js +243 -0
- package/node_modules/jose/dist/webapi/lib/base64.js +22 -0
- package/node_modules/jose/dist/webapi/lib/buffer_utils.js +43 -0
- package/node_modules/jose/dist/webapi/lib/check_key_type.js +122 -0
- package/node_modules/jose/dist/webapi/lib/content_encryption.js +217 -0
- package/node_modules/jose/dist/webapi/lib/crypto_key.js +136 -0
- package/node_modules/jose/dist/webapi/lib/deflate.js +44 -0
- package/node_modules/jose/dist/webapi/lib/ecdhes.js +52 -0
- package/node_modules/jose/dist/webapi/lib/helpers.js +19 -0
- package/node_modules/jose/dist/webapi/lib/invalid_key_input.js +27 -0
- package/node_modules/jose/dist/webapi/lib/is_key_like.js +17 -0
- package/node_modules/jose/dist/webapi/lib/jwk_to_key.js +107 -0
- package/node_modules/jose/dist/webapi/lib/jwt_claims_set.js +238 -0
- package/node_modules/jose/dist/webapi/lib/key_management.js +186 -0
- package/node_modules/jose/dist/webapi/lib/key_to_jwk.js +31 -0
- package/node_modules/jose/dist/webapi/lib/normalize_key.js +166 -0
- package/node_modules/jose/dist/webapi/lib/pbes2kw.js +42 -0
- package/node_modules/jose/dist/webapi/lib/rsaes.js +24 -0
- package/node_modules/jose/dist/webapi/lib/signing.js +68 -0
- package/node_modules/jose/dist/webapi/lib/type_checks.js +40 -0
- package/node_modules/jose/dist/webapi/lib/validate_algorithms.js +10 -0
- package/node_modules/jose/dist/webapi/lib/validate_crit.js +33 -0
- package/node_modules/jose/dist/webapi/util/base64url.js +30 -0
- package/node_modules/jose/dist/webapi/util/decode_jwt.js +32 -0
- package/node_modules/jose/dist/webapi/util/decode_protected_header.js +34 -0
- package/node_modules/jose/dist/webapi/util/errors.js +99 -0
- package/node_modules/jose/package.json +200 -0
- package/node_modules/tiny-http-mcp-server/LICENSE +21 -0
- package/node_modules/tiny-http-mcp-server/README.md +778 -0
- package/node_modules/tiny-http-mcp-server/dist/auth.d.ts +69 -0
- package/node_modules/tiny-http-mcp-server/dist/auth.js +261 -0
- package/node_modules/tiny-http-mcp-server/dist/cli.d.ts +20 -0
- package/node_modules/tiny-http-mcp-server/dist/cli.js +465 -0
- package/node_modules/tiny-http-mcp-server/dist/composition.json +30 -0
- package/node_modules/tiny-http-mcp-server/dist/express-middleware.d.ts +18 -0
- package/node_modules/tiny-http-mcp-server/dist/express-middleware.js +90 -0
- package/node_modules/tiny-http-mcp-server/dist/http-server.d.ts +49 -0
- package/node_modules/tiny-http-mcp-server/dist/http-server.js +264 -0
- package/node_modules/tiny-http-mcp-server/dist/http-transport.d.ts +164 -0
- package/node_modules/tiny-http-mcp-server/dist/http-transport.js +897 -0
- package/node_modules/tiny-http-mcp-server/dist/index.d.ts +13 -0
- package/node_modules/tiny-http-mcp-server/dist/index.js +6 -0
- package/node_modules/tiny-http-mcp-server/dist/load-oauth-verifier.d.ts +6 -0
- package/node_modules/tiny-http-mcp-server/dist/load-oauth-verifier.js +43 -0
- package/node_modules/tiny-http-mcp-server/dist/parse-body.d.ts +22 -0
- package/node_modules/tiny-http-mcp-server/dist/parse-body.js +150 -0
- package/node_modules/tiny-http-mcp-server/dist/server.d.ts +9 -0
- package/node_modules/tiny-http-mcp-server/dist/server.js +4 -0
- package/node_modules/tiny-http-mcp-server/dist/session.d.ts +18 -0
- package/node_modules/tiny-http-mcp-server/dist/session.js +37 -0
- package/node_modules/tiny-http-mcp-server/dist/sse.d.ts +11 -0
- package/node_modules/tiny-http-mcp-server/dist/sse.js +22 -0
- package/node_modules/tiny-http-mcp-server/dist/test-support.d.ts +10 -0
- package/node_modules/tiny-http-mcp-server/dist/test-support.js +398 -0
- package/node_modules/tiny-http-mcp-server/dist/testing.d.ts +59 -0
- package/node_modules/tiny-http-mcp-server/dist/testing.js +191 -0
- package/node_modules/tiny-http-mcp-server/node_modules/mcp-oauth/LICENSE +21 -0
- package/node_modules/tiny-http-mcp-server/node_modules/mcp-oauth/README.md +70 -0
- package/node_modules/tiny-http-mcp-server/node_modules/mcp-oauth/dist/client/auth-store-session-store.d.ts +14 -0
- package/node_modules/tiny-http-mcp-server/node_modules/mcp-oauth/dist/client/auth-store-session-store.js +169 -0
- package/node_modules/tiny-http-mcp-server/node_modules/mcp-oauth/dist/client/authorization-state.d.ts +8 -0
- package/node_modules/tiny-http-mcp-server/node_modules/mcp-oauth/dist/client/authorization-state.js +47 -0
- package/node_modules/tiny-http-mcp-server/node_modules/mcp-oauth/dist/client/default-oauth-client-provider.d.ts +3 -0
- package/node_modules/tiny-http-mcp-server/node_modules/mcp-oauth/dist/client/default-oauth-client-provider.js +627 -0
- package/node_modules/tiny-http-mcp-server/node_modules/mcp-oauth/dist/client/loopback-authorization.d.ts +20 -0
- package/node_modules/tiny-http-mcp-server/node_modules/mcp-oauth/dist/client/loopback-authorization.js +207 -0
- package/node_modules/tiny-http-mcp-server/node_modules/mcp-oauth/dist/client/pkce.d.ts +2 -0
- package/node_modules/tiny-http-mcp-server/node_modules/mcp-oauth/dist/client/pkce.js +7 -0
- package/node_modules/tiny-http-mcp-server/node_modules/mcp-oauth/dist/client/token-endpoint.d.ts +40 -0
- package/node_modules/tiny-http-mcp-server/node_modules/mcp-oauth/dist/client/token-endpoint.js +164 -0
- package/node_modules/tiny-http-mcp-server/node_modules/mcp-oauth/dist/client/types.d.ts +113 -0
- package/node_modules/tiny-http-mcp-server/node_modules/mcp-oauth/dist/client/types.js +1 -0
- package/node_modules/tiny-http-mcp-server/node_modules/mcp-oauth/dist/index.d.ts +10 -0
- package/node_modules/tiny-http-mcp-server/node_modules/mcp-oauth/dist/index.js +7 -0
- package/node_modules/tiny-http-mcp-server/node_modules/mcp-oauth/dist/resource-indicator.d.ts +1 -0
- package/node_modules/tiny-http-mcp-server/node_modules/mcp-oauth/dist/resource-indicator.js +11 -0
- package/node_modules/tiny-http-mcp-server/node_modules/mcp-oauth/dist/server/jwks-token-verifier.d.ts +32 -0
- package/node_modules/tiny-http-mcp-server/node_modules/mcp-oauth/dist/server/jwks-token-verifier.js +388 -0
- package/node_modules/tiny-http-mcp-server/node_modules/mcp-oauth/dist/types.compile-check.d.ts +1 -0
- package/node_modules/tiny-http-mcp-server/node_modules/mcp-oauth/dist/types.compile-check.js +22 -0
- package/node_modules/tiny-http-mcp-server/node_modules/mcp-oauth/package.json +28 -0
- package/node_modules/tiny-http-mcp-server/package.json +59 -0
- package/node_modules/tiny-stdio-mcp-server/dist/composition.json +1 -1
- package/node_modules/toolcraft-schema/package.json +1 -1
- package/package.json +11 -4
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
import http from "node:http";
|
|
2
|
+
import https from "node:https";
|
|
3
|
+
import { EventEmitter } from "node:events";
|
|
4
|
+
import { Readable } from "node:stream";
|
|
5
|
+
import { Audio, defineSchema, File, Image } from "tiny-stdio-mcp-server";
|
|
6
|
+
import { createHttpServer } from "./http-server.js";
|
|
7
|
+
const TEST_PNG_BASE64 = "iVBORw0KGgo=";
|
|
8
|
+
const TEST_MP3_BASE64 = "SUQzBAAAAAA=";
|
|
9
|
+
const IN_MEMORY_HTTP_STATE = Symbol.for("tiny-http-mcp-server.in-memory-http-state");
|
|
10
|
+
const IN_MEMORY_HTTP_META = Symbol.for("tiny-http-mcp-server.in-memory-http-meta");
|
|
11
|
+
function getInMemoryHttpState() {
|
|
12
|
+
const target = globalThis;
|
|
13
|
+
if (target[IN_MEMORY_HTTP_STATE] === undefined) {
|
|
14
|
+
target[IN_MEMORY_HTTP_STATE] = {
|
|
15
|
+
installed: false,
|
|
16
|
+
nextPort: 41_000,
|
|
17
|
+
servers: new Map()
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
return target[IN_MEMORY_HTTP_STATE];
|
|
21
|
+
}
|
|
22
|
+
function normalizeRequestHostname(hostname) {
|
|
23
|
+
if (hostname.startsWith("[") && hostname.endsWith("]")) {
|
|
24
|
+
return hostname.slice(1, -1);
|
|
25
|
+
}
|
|
26
|
+
return hostname;
|
|
27
|
+
}
|
|
28
|
+
function formatHostnameForOrigin(hostname) {
|
|
29
|
+
return hostname.includes(":") && !hostname.startsWith("[") ? `[${hostname}]` : hostname;
|
|
30
|
+
}
|
|
31
|
+
function normalizeListenHostname(hostname) {
|
|
32
|
+
if (hostname === undefined || hostname.length === 0) {
|
|
33
|
+
return "127.0.0.1";
|
|
34
|
+
}
|
|
35
|
+
return hostname === "localhost" ? "127.0.0.1" : hostname;
|
|
36
|
+
}
|
|
37
|
+
function parseListenArgs(args) {
|
|
38
|
+
const callback = args.find((arg) => typeof arg === "function");
|
|
39
|
+
const options = args.find((arg) => typeof arg === "object" && arg !== null && !Array.isArray(arg));
|
|
40
|
+
if (options !== undefined) {
|
|
41
|
+
return {
|
|
42
|
+
port: options.port ?? 0,
|
|
43
|
+
hostname: normalizeListenHostname(options.host ?? options.hostname),
|
|
44
|
+
callback
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
const port = typeof args[0] === "number" ? args[0] : 0;
|
|
48
|
+
const hostname = typeof args[1] === "string" ? args[1] : undefined;
|
|
49
|
+
return {
|
|
50
|
+
port,
|
|
51
|
+
hostname: normalizeListenHostname(hostname),
|
|
52
|
+
callback
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
export function installInMemoryHttp() {
|
|
56
|
+
const state = getInMemoryHttpState();
|
|
57
|
+
if (state.installed) {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
state.installed = true;
|
|
61
|
+
const originalAddress = http.Server.prototype.address;
|
|
62
|
+
const originalClose = http.Server.prototype.close;
|
|
63
|
+
const originalFetch = globalThis.fetch.bind(globalThis);
|
|
64
|
+
http.Server.prototype.listen = function listen(...args) {
|
|
65
|
+
const { port: requestedPort, hostname, callback } = parseListenArgs(args);
|
|
66
|
+
const port = requestedPort === 0 ? state.nextPort++ : requestedPort;
|
|
67
|
+
const address = normalizeListenHostname(hostname);
|
|
68
|
+
const origin = `http://${formatHostnameForOrigin(address)}:${port}`;
|
|
69
|
+
const meta = {
|
|
70
|
+
origin,
|
|
71
|
+
address: {
|
|
72
|
+
address,
|
|
73
|
+
family: address.includes(":") ? "IPv6" : "IPv4",
|
|
74
|
+
port
|
|
75
|
+
},
|
|
76
|
+
listening: true
|
|
77
|
+
};
|
|
78
|
+
this[IN_MEMORY_HTTP_META] = meta;
|
|
79
|
+
state.servers.set(origin, this);
|
|
80
|
+
Object.defineProperty(this, "listening", {
|
|
81
|
+
configurable: true,
|
|
82
|
+
get() {
|
|
83
|
+
return this[IN_MEMORY_HTTP_META]?.listening ?? false;
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
queueMicrotask(() => {
|
|
87
|
+
this.emit("listening");
|
|
88
|
+
callback?.();
|
|
89
|
+
});
|
|
90
|
+
return this;
|
|
91
|
+
};
|
|
92
|
+
http.Server.prototype.address = function address() {
|
|
93
|
+
return this[IN_MEMORY_HTTP_META]?.address ?? originalAddress.call(this);
|
|
94
|
+
};
|
|
95
|
+
http.Server.prototype.close = function close(callback) {
|
|
96
|
+
const meta = this[IN_MEMORY_HTTP_META];
|
|
97
|
+
if (meta === undefined) {
|
|
98
|
+
return originalClose.call(this, callback);
|
|
99
|
+
}
|
|
100
|
+
if (meta.listening) {
|
|
101
|
+
state.servers.delete(meta.origin);
|
|
102
|
+
meta.listening = false;
|
|
103
|
+
}
|
|
104
|
+
queueMicrotask(() => {
|
|
105
|
+
callback?.();
|
|
106
|
+
this.emit("close");
|
|
107
|
+
});
|
|
108
|
+
return this;
|
|
109
|
+
};
|
|
110
|
+
globalThis.fetch = async (input, init = {}) => {
|
|
111
|
+
const normalized = normalizeFetchInput(input, init);
|
|
112
|
+
const inMemoryServer = getInMemoryServer(normalized.url);
|
|
113
|
+
if (inMemoryServer !== undefined) {
|
|
114
|
+
return fetchInMemory(inMemoryServer, normalized.url, normalized.init);
|
|
115
|
+
}
|
|
116
|
+
return originalFetch(input, init);
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
function getInMemoryServer(url) {
|
|
120
|
+
const state = globalThis[IN_MEMORY_HTTP_STATE];
|
|
121
|
+
if (state?.installed !== true) {
|
|
122
|
+
return undefined;
|
|
123
|
+
}
|
|
124
|
+
return state.servers.get(url.origin);
|
|
125
|
+
}
|
|
126
|
+
async function readFetchBody(body) {
|
|
127
|
+
if (body === undefined || body === null) {
|
|
128
|
+
return undefined;
|
|
129
|
+
}
|
|
130
|
+
if (typeof body === "string") {
|
|
131
|
+
return body;
|
|
132
|
+
}
|
|
133
|
+
if (body instanceof Uint8Array) {
|
|
134
|
+
return Buffer.from(body).toString("utf8");
|
|
135
|
+
}
|
|
136
|
+
if (body instanceof URLSearchParams) {
|
|
137
|
+
return body.toString();
|
|
138
|
+
}
|
|
139
|
+
if (body instanceof ReadableStream) {
|
|
140
|
+
return new Response(body).text();
|
|
141
|
+
}
|
|
142
|
+
return String(body);
|
|
143
|
+
}
|
|
144
|
+
async function fetchInMemory(server, url, init) {
|
|
145
|
+
const headers = new Headers(init.headers);
|
|
146
|
+
if (!headers.has("host")) {
|
|
147
|
+
headers.set("host", url.host);
|
|
148
|
+
}
|
|
149
|
+
const body = await readFetchBody(init.body);
|
|
150
|
+
const request = Object.assign(Readable.from(body === undefined ? [] : [body]), {
|
|
151
|
+
method: init.method ?? "GET",
|
|
152
|
+
url: `${url.pathname}${url.search}`,
|
|
153
|
+
headers: Object.fromEntries(headers.entries()),
|
|
154
|
+
socket: {}
|
|
155
|
+
});
|
|
156
|
+
const response = new EventEmitter();
|
|
157
|
+
let streamController;
|
|
158
|
+
const responseBody = new ReadableStream({
|
|
159
|
+
start(controller) {
|
|
160
|
+
streamController = controller;
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
response.statusCode = 200;
|
|
164
|
+
response.statusMessage = "OK";
|
|
165
|
+
response.chunks = [];
|
|
166
|
+
response.headerValues = new Headers();
|
|
167
|
+
response.headersSent = false;
|
|
168
|
+
response.writableEnded = false;
|
|
169
|
+
const setResponseHeader = (name, value) => {
|
|
170
|
+
response.headerValues.delete(name);
|
|
171
|
+
if (Array.isArray(value) && name.toLowerCase() === "set-cookie") {
|
|
172
|
+
for (const entry of value) {
|
|
173
|
+
response.headerValues.append(name, entry);
|
|
174
|
+
}
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
if (Array.isArray(value)) {
|
|
178
|
+
response.headerValues.set(name, value.join(", "));
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
response.headerValues.set(name, String(value));
|
|
182
|
+
};
|
|
183
|
+
response.setHeader = ((name, value) => {
|
|
184
|
+
setResponseHeader(name, value);
|
|
185
|
+
return response;
|
|
186
|
+
});
|
|
187
|
+
response.getHeader = ((name) => {
|
|
188
|
+
return response.headerValues.get(name) ?? undefined;
|
|
189
|
+
});
|
|
190
|
+
response.getHeaders = (() => {
|
|
191
|
+
return Object.fromEntries(response.headerValues.entries());
|
|
192
|
+
});
|
|
193
|
+
response.removeHeader = ((name) => {
|
|
194
|
+
response.headerValues.delete(name);
|
|
195
|
+
});
|
|
196
|
+
response.writeHead = ((statusCode, responseHeaders) => {
|
|
197
|
+
response.statusCode = statusCode;
|
|
198
|
+
response.headersSent = true;
|
|
199
|
+
for (const [key, value] of Object.entries(responseHeaders ?? {})) {
|
|
200
|
+
setResponseHeader(key, value);
|
|
201
|
+
}
|
|
202
|
+
return response;
|
|
203
|
+
});
|
|
204
|
+
response.write = ((chunk) => {
|
|
205
|
+
const bytes = typeof chunk === "string" ? Buffer.from(chunk) : Buffer.from(chunk);
|
|
206
|
+
response.chunks.push(bytes);
|
|
207
|
+
streamController?.enqueue(bytes);
|
|
208
|
+
return true;
|
|
209
|
+
});
|
|
210
|
+
response.end = ((chunk) => {
|
|
211
|
+
if (chunk !== undefined) {
|
|
212
|
+
response.write(chunk);
|
|
213
|
+
}
|
|
214
|
+
if (!response.writableEnded) {
|
|
215
|
+
response.writableEnded = true;
|
|
216
|
+
try {
|
|
217
|
+
streamController?.close();
|
|
218
|
+
}
|
|
219
|
+
catch {
|
|
220
|
+
// Already closed by an abort.
|
|
221
|
+
}
|
|
222
|
+
response.emit("finish");
|
|
223
|
+
response.emit("close");
|
|
224
|
+
}
|
|
225
|
+
return response;
|
|
226
|
+
});
|
|
227
|
+
response.flushHeaders = (() => {
|
|
228
|
+
response.headersSent = true;
|
|
229
|
+
});
|
|
230
|
+
const signal = init.signal ?? undefined;
|
|
231
|
+
if (signal !== undefined) {
|
|
232
|
+
const abort = () => {
|
|
233
|
+
response.end();
|
|
234
|
+
};
|
|
235
|
+
if (signal.aborted) {
|
|
236
|
+
abort();
|
|
237
|
+
}
|
|
238
|
+
else {
|
|
239
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
server.emit("request", request, response);
|
|
243
|
+
await new Promise((resolve) => {
|
|
244
|
+
if (response.headersSent || response.writableEnded) {
|
|
245
|
+
resolve();
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
response.flushHeaders = (() => {
|
|
249
|
+
response.headersSent = true;
|
|
250
|
+
resolve();
|
|
251
|
+
});
|
|
252
|
+
response.once("finish", resolve);
|
|
253
|
+
});
|
|
254
|
+
const responseContent = response.writableEnded ? Buffer.concat(response.chunks) : responseBody;
|
|
255
|
+
return new Response(response.statusCode === 204 ? null : responseContent, {
|
|
256
|
+
status: response.statusCode,
|
|
257
|
+
statusText: response.statusMessage,
|
|
258
|
+
headers: response.headerValues
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
function normalizeFetchInput(input, init = {}) {
|
|
262
|
+
if (input instanceof Request) {
|
|
263
|
+
return {
|
|
264
|
+
url: new URL(input.url),
|
|
265
|
+
init: {
|
|
266
|
+
...init,
|
|
267
|
+
method: init.method ?? input.method,
|
|
268
|
+
headers: init.headers ?? input.headers,
|
|
269
|
+
body: init.body ?? input.body,
|
|
270
|
+
signal: init.signal ?? input.signal
|
|
271
|
+
}
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
return {
|
|
275
|
+
url: new URL(String(input)),
|
|
276
|
+
init
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
export async function nodeFetch(input, init = {}) {
|
|
280
|
+
const { url, init: normalizedInit } = normalizeFetchInput(input, init);
|
|
281
|
+
const inMemoryServer = getInMemoryServer(url);
|
|
282
|
+
if (inMemoryServer !== undefined) {
|
|
283
|
+
return fetchInMemory(inMemoryServer, url, normalizedInit);
|
|
284
|
+
}
|
|
285
|
+
const client = url.protocol === "https:" ? https : http;
|
|
286
|
+
const headers = new Headers(normalizedInit.headers);
|
|
287
|
+
return new Promise((resolve, reject) => {
|
|
288
|
+
const request = client.request({
|
|
289
|
+
method: normalizedInit.method ?? "GET",
|
|
290
|
+
hostname: normalizeRequestHostname(url.hostname),
|
|
291
|
+
port: url.port.length > 0 ? Number(url.port) : url.protocol === "https:" ? 443 : 80,
|
|
292
|
+
path: `${url.pathname}${url.search}`,
|
|
293
|
+
headers: Object.fromEntries(headers.entries())
|
|
294
|
+
}, (response) => {
|
|
295
|
+
const responseHeaders = new Headers();
|
|
296
|
+
for (const [key, value] of Object.entries(response.headers)) {
|
|
297
|
+
if (typeof value === "string") {
|
|
298
|
+
responseHeaders.set(key, value);
|
|
299
|
+
continue;
|
|
300
|
+
}
|
|
301
|
+
if (Array.isArray(value)) {
|
|
302
|
+
for (const entry of value) {
|
|
303
|
+
responseHeaders.append(key, entry);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
const body = response.statusCode === 204
|
|
308
|
+
? null
|
|
309
|
+
: Readable.toWeb(response);
|
|
310
|
+
resolve(new Response(body, {
|
|
311
|
+
status: response.statusCode ?? 0,
|
|
312
|
+
statusText: response.statusMessage ?? "",
|
|
313
|
+
headers: responseHeaders
|
|
314
|
+
}));
|
|
315
|
+
});
|
|
316
|
+
request.on("error", reject);
|
|
317
|
+
const signal = normalizedInit.signal ?? undefined;
|
|
318
|
+
if (signal !== undefined) {
|
|
319
|
+
const onAbort = () => {
|
|
320
|
+
request.destroy(new Error("Request aborted"));
|
|
321
|
+
};
|
|
322
|
+
if (signal.aborted) {
|
|
323
|
+
onAbort();
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
327
|
+
request.once("close", () => {
|
|
328
|
+
signal.removeEventListener("abort", onAbort);
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
if (typeof normalizedInit.body === "string" || normalizedInit.body instanceof Uint8Array) {
|
|
332
|
+
request.write(normalizedInit.body);
|
|
333
|
+
}
|
|
334
|
+
else if (normalizedInit.body instanceof URLSearchParams) {
|
|
335
|
+
request.write(normalizedInit.body.toString());
|
|
336
|
+
}
|
|
337
|
+
request.end();
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
export function createTestMcpServer(options = {}) {
|
|
341
|
+
const emptySchema = defineSchema({});
|
|
342
|
+
const textSchema = defineSchema({
|
|
343
|
+
text: { type: "string" }
|
|
344
|
+
});
|
|
345
|
+
return (createHttpServer({
|
|
346
|
+
name: options.name ?? "conformance-test-server",
|
|
347
|
+
version: options.version ?? "1.0.0",
|
|
348
|
+
...(hasOwnProperty(options, "enableJsonResponse")
|
|
349
|
+
? { enableJsonResponse: options.enableJsonResponse }
|
|
350
|
+
: {}),
|
|
351
|
+
...(hasOwnProperty(options, "sessionIdGenerator")
|
|
352
|
+
? { sessionIdGenerator: options.sessionIdGenerator }
|
|
353
|
+
: {}),
|
|
354
|
+
...(hasOwnProperty(options, "oauth") ? { oauth: options.oauth } : {})
|
|
355
|
+
})
|
|
356
|
+
.tool("echo", "Echo input text", textSchema, ({ text }) => String(text))
|
|
357
|
+
.tool("reverse", "Reverse input text", textSchema, ({ text }) => String(text).split("").reverse().join(""))
|
|
358
|
+
.tool("uppercase", "Uppercase input text", textSchema, ({ text }) => String(text).toUpperCase())
|
|
359
|
+
.tool("get_user", "Return a test user object", defineSchema({ id: { type: "string" } }), ({ id }) => ({
|
|
360
|
+
id: String(id),
|
|
361
|
+
name: "Alice",
|
|
362
|
+
role: "admin"
|
|
363
|
+
}), {
|
|
364
|
+
type: "object",
|
|
365
|
+
properties: {
|
|
366
|
+
id: { type: "string" },
|
|
367
|
+
name: { type: "string" },
|
|
368
|
+
role: { type: "string" }
|
|
369
|
+
},
|
|
370
|
+
required: ["id", "name", "role"],
|
|
371
|
+
additionalProperties: false
|
|
372
|
+
})
|
|
373
|
+
// Deliberately untyped fixture: exercises legacy JSON-array text fallback.
|
|
374
|
+
.tool("get_list", "Return a numeric array", emptySchema, () => [1, 2, 3])
|
|
375
|
+
.tool("get_image", "Return an image block", emptySchema, () => Image.fromBase64(TEST_PNG_BASE64, "image/png"))
|
|
376
|
+
.tool("get_audio", "Return an audio block", emptySchema, () => Audio.fromBase64(TEST_MP3_BASE64, "audio/mpeg"))
|
|
377
|
+
.tool("get_file", "Return a file block", emptySchema, () => File.fromText("hello,world", "text/csv"))
|
|
378
|
+
.tool("get_mixed", "Return multiple content blocks", emptySchema, () => [
|
|
379
|
+
Image.fromBase64(TEST_PNG_BASE64, "image/png"),
|
|
380
|
+
"Caption for the image",
|
|
381
|
+
File.fromText("notes")
|
|
382
|
+
])
|
|
383
|
+
.tool("throw_sync", "Throw synchronously", emptySchema, () => {
|
|
384
|
+
throw new Error("sync boom");
|
|
385
|
+
})
|
|
386
|
+
.tool("throw_async", "Throw asynchronously", emptySchema, async () => {
|
|
387
|
+
throw new Error("async boom");
|
|
388
|
+
})
|
|
389
|
+
.tool("empty_result", "Return undefined", emptySchema, () => undefined)
|
|
390
|
+
.tool("slow", "Resolve slowly", emptySchema, async () => {
|
|
391
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
392
|
+
return "done";
|
|
393
|
+
})
|
|
394
|
+
.tool("large_output", "Return 100KB of text", emptySchema, () => "x".repeat(100_000)));
|
|
395
|
+
}
|
|
396
|
+
function hasOwnProperty(value, key) {
|
|
397
|
+
return Object.prototype.hasOwnProperty.call(value, key);
|
|
398
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
2
|
+
import type { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
3
|
+
import type { HttpServer, HttpServerHandle } from "./http-server.js";
|
|
4
|
+
import { type TokenVerifier } from "./auth.js";
|
|
5
|
+
export { createTestMcpServer, installInMemoryHttp, nodeFetch } from "./test-support.js";
|
|
6
|
+
export interface HttpTestPair {
|
|
7
|
+
client: Client;
|
|
8
|
+
transport: StreamableHTTPClientTransport;
|
|
9
|
+
handle: HttpServerHandle;
|
|
10
|
+
url: string;
|
|
11
|
+
cleanup(): Promise<void>;
|
|
12
|
+
}
|
|
13
|
+
export interface TinyHttpRequestLogEntry {
|
|
14
|
+
method: string;
|
|
15
|
+
sessionId: string | null;
|
|
16
|
+
jsonRpcMethod?: string;
|
|
17
|
+
responseContentType?: string | null;
|
|
18
|
+
}
|
|
19
|
+
export interface TinyHttpTestPair {
|
|
20
|
+
client: {
|
|
21
|
+
listTools(): Promise<{
|
|
22
|
+
tools: Array<{
|
|
23
|
+
name: string;
|
|
24
|
+
}>;
|
|
25
|
+
}>;
|
|
26
|
+
callTool(params: {
|
|
27
|
+
name: string;
|
|
28
|
+
arguments?: Record<string, unknown>;
|
|
29
|
+
}): Promise<{
|
|
30
|
+
content: unknown[];
|
|
31
|
+
isError?: boolean;
|
|
32
|
+
}>;
|
|
33
|
+
close(): Promise<void>;
|
|
34
|
+
};
|
|
35
|
+
transport: unknown;
|
|
36
|
+
handle: HttpServerHandle;
|
|
37
|
+
url: string;
|
|
38
|
+
requests: TinyHttpRequestLogEntry[];
|
|
39
|
+
cleanup(): Promise<void>;
|
|
40
|
+
}
|
|
41
|
+
export interface InMemoryAccessTokenInput {
|
|
42
|
+
token?: string;
|
|
43
|
+
issuer: string;
|
|
44
|
+
audience: readonly string[];
|
|
45
|
+
scopes: readonly string[];
|
|
46
|
+
expiresAt: number;
|
|
47
|
+
claims?: Record<string, unknown>;
|
|
48
|
+
subject?: string;
|
|
49
|
+
clientId?: string;
|
|
50
|
+
}
|
|
51
|
+
export interface InMemoryTokenVerifier {
|
|
52
|
+
verifier: TokenVerifier;
|
|
53
|
+
issueToken(input: InMemoryAccessTokenInput): string;
|
|
54
|
+
}
|
|
55
|
+
export declare function createInMemoryTokenVerifier(options?: Partial<{
|
|
56
|
+
now: () => number;
|
|
57
|
+
}>): InMemoryTokenVerifier;
|
|
58
|
+
export declare function createHttpTestPair(server: HttpServer): Promise<HttpTestPair>;
|
|
59
|
+
export declare function createHttpTestPairWithTinyClient(server: HttpServer): Promise<TinyHttpTestPair>;
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { TokenVerificationError } from "./auth.js";
|
|
2
|
+
import { installInMemoryHttp, nodeFetch } from "./test-support.js";
|
|
3
|
+
export { createTestMcpServer, installInMemoryHttp, nodeFetch } from "./test-support.js";
|
|
4
|
+
function cloneVerifiedAccessToken(token) {
|
|
5
|
+
return {
|
|
6
|
+
...token,
|
|
7
|
+
audience: [...token.audience],
|
|
8
|
+
scopes: [...token.scopes],
|
|
9
|
+
claims: structuredClone(token.claims)
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
export function createInMemoryTokenVerifier(options = {}) {
|
|
13
|
+
const tokens = new Map();
|
|
14
|
+
const now = options.now ?? (() => Math.floor(Date.now() / 1_000));
|
|
15
|
+
let nextTokenId = 1;
|
|
16
|
+
return {
|
|
17
|
+
verifier: {
|
|
18
|
+
async verify(input) {
|
|
19
|
+
const token = tokens.get(input.token);
|
|
20
|
+
if (token === undefined) {
|
|
21
|
+
throw new TokenVerificationError({
|
|
22
|
+
error: "invalid_token",
|
|
23
|
+
errorDescription: "unknown token"
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
if (!input.authorizationServers.includes(token.issuer)) {
|
|
27
|
+
throw new TokenVerificationError({
|
|
28
|
+
error: "invalid_token",
|
|
29
|
+
errorDescription: "issuer mismatch"
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
if (!token.audience.includes(input.resource)) {
|
|
33
|
+
throw new TokenVerificationError({
|
|
34
|
+
error: "invalid_token",
|
|
35
|
+
errorDescription: "audience mismatch"
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
if (token.expiresAt <= now()) {
|
|
39
|
+
throw new TokenVerificationError({
|
|
40
|
+
error: "invalid_token",
|
|
41
|
+
errorDescription: "token expired"
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
if (input.requiredScopes.length > 0 &&
|
|
45
|
+
!token.scopes.some((scope) => input.requiredScopes.includes(scope))) {
|
|
46
|
+
throw new TokenVerificationError({
|
|
47
|
+
error: "insufficient_scope",
|
|
48
|
+
errorDescription: "insufficient scope",
|
|
49
|
+
scope: input.requiredScopes
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
return cloneVerifiedAccessToken(token);
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
issueToken(input) {
|
|
56
|
+
const token = input.token ?? `test-token-${nextTokenId++}`;
|
|
57
|
+
if (tokens.has(token)) {
|
|
58
|
+
throw new Error(`Token has already been issued: ${token}`);
|
|
59
|
+
}
|
|
60
|
+
const audience = [...input.audience];
|
|
61
|
+
const scopes = [...input.scopes];
|
|
62
|
+
const claims = {
|
|
63
|
+
...(input.claims ?? {}),
|
|
64
|
+
iss: input.issuer,
|
|
65
|
+
aud: audience.length === 1 ? audience[0] : audience,
|
|
66
|
+
exp: input.expiresAt,
|
|
67
|
+
scope: scopes.join(" "),
|
|
68
|
+
...(input.subject === undefined
|
|
69
|
+
? {}
|
|
70
|
+
: {
|
|
71
|
+
sub: input.subject
|
|
72
|
+
}),
|
|
73
|
+
...(input.clientId === undefined
|
|
74
|
+
? {}
|
|
75
|
+
: {
|
|
76
|
+
client_id: input.clientId
|
|
77
|
+
})
|
|
78
|
+
};
|
|
79
|
+
tokens.set(token, {
|
|
80
|
+
token,
|
|
81
|
+
issuer: input.issuer,
|
|
82
|
+
audience,
|
|
83
|
+
scopes,
|
|
84
|
+
expiresAt: input.expiresAt,
|
|
85
|
+
claims,
|
|
86
|
+
...(input.subject === undefined
|
|
87
|
+
? {}
|
|
88
|
+
: {
|
|
89
|
+
subject: input.subject
|
|
90
|
+
}),
|
|
91
|
+
...(input.clientId === undefined
|
|
92
|
+
? {}
|
|
93
|
+
: {
|
|
94
|
+
clientId: input.clientId
|
|
95
|
+
})
|
|
96
|
+
});
|
|
97
|
+
return token;
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
export async function createHttpTestPair(server) {
|
|
102
|
+
let sdkClient;
|
|
103
|
+
let sdkTransport;
|
|
104
|
+
try {
|
|
105
|
+
sdkClient = await import("@modelcontextprotocol/sdk/client/index.js");
|
|
106
|
+
sdkTransport = await import("@modelcontextprotocol/sdk/client/streamableHttp.js");
|
|
107
|
+
}
|
|
108
|
+
catch (error) {
|
|
109
|
+
throw new Error("createHttpTestPair requires @modelcontextprotocol/sdk; install it as a devDependency or use createHttpTestPairWithTinyClient", { cause: error });
|
|
110
|
+
}
|
|
111
|
+
installInMemoryHttp();
|
|
112
|
+
const handle = await server.listenHttp({ port: 0 });
|
|
113
|
+
const client = new sdkClient.Client({ name: "sdk-test-client", version: "1.0.0" });
|
|
114
|
+
const transport = new sdkTransport.StreamableHTTPClientTransport(new URL(handle.url), {
|
|
115
|
+
fetch: nodeFetch
|
|
116
|
+
});
|
|
117
|
+
try {
|
|
118
|
+
await client.connect(transport);
|
|
119
|
+
}
|
|
120
|
+
catch (error) {
|
|
121
|
+
await handle.close();
|
|
122
|
+
throw error;
|
|
123
|
+
}
|
|
124
|
+
return {
|
|
125
|
+
client,
|
|
126
|
+
transport,
|
|
127
|
+
handle,
|
|
128
|
+
url: handle.url,
|
|
129
|
+
cleanup: async () => {
|
|
130
|
+
const results = await Promise.allSettled([client.close(), handle.close()]);
|
|
131
|
+
const rejected = results.find((result) => result.status === "rejected");
|
|
132
|
+
if (rejected !== undefined) {
|
|
133
|
+
throw rejected.reason;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
export async function createHttpTestPairWithTinyClient(server) {
|
|
139
|
+
let tinyMcpClient;
|
|
140
|
+
try {
|
|
141
|
+
tinyMcpClient = await import("tiny-mcp-client");
|
|
142
|
+
}
|
|
143
|
+
catch (error) {
|
|
144
|
+
throw new Error("createHttpTestPairWithTinyClient requires tiny-mcp-client; install tiny-mcp-client as a devDependency", { cause: error });
|
|
145
|
+
}
|
|
146
|
+
installInMemoryHttp();
|
|
147
|
+
const handle = await server.listenHttp({ port: 0 });
|
|
148
|
+
const requests = [];
|
|
149
|
+
const client = new tinyMcpClient.McpClient({
|
|
150
|
+
clientInfo: { name: "tiny-http-test-client", version: "1.0.0" }
|
|
151
|
+
});
|
|
152
|
+
const transport = new tinyMcpClient.HttpTransport({
|
|
153
|
+
url: handle.url,
|
|
154
|
+
fetch: async (input, init = {}) => {
|
|
155
|
+
const headers = new Headers(init.headers);
|
|
156
|
+
const method = init.method ?? "GET";
|
|
157
|
+
let jsonRpcMethod;
|
|
158
|
+
if (typeof init.body === "string" && init.body.length > 0) {
|
|
159
|
+
try {
|
|
160
|
+
const parsed = JSON.parse(init.body);
|
|
161
|
+
if (typeof parsed.method === "string") {
|
|
162
|
+
jsonRpcMethod = parsed.method;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
catch {
|
|
166
|
+
jsonRpcMethod = undefined;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
const response = await nodeFetch(String(input), init);
|
|
170
|
+
requests.push({
|
|
171
|
+
method,
|
|
172
|
+
sessionId: headers.get("mcp-session-id"),
|
|
173
|
+
jsonRpcMethod,
|
|
174
|
+
responseContentType: response.headers.get("content-type")
|
|
175
|
+
});
|
|
176
|
+
return response;
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
await client.connect(transport);
|
|
180
|
+
return {
|
|
181
|
+
client,
|
|
182
|
+
transport,
|
|
183
|
+
handle,
|
|
184
|
+
url: handle.url,
|
|
185
|
+
requests,
|
|
186
|
+
cleanup: async () => {
|
|
187
|
+
await client.close();
|
|
188
|
+
await handle.close();
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Poe Platform
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|