open-mcp-app 0.1.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.
Files changed (4) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +36 -0
  3. package/index.js +363 -0
  4. package/package.json +32 -0
package/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026
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.
22
+
package/README.md ADDED
@@ -0,0 +1,36 @@
1
+ # open-mcp-app
2
+
3
+ A small Node.js SDK skeleton for working with open MCP apps.
4
+
5
+ This package is intentionally lightweight: it includes a generic HTTP `request` primitive and a few convenience methods that demonstrate typical SDK call shapes against an "open apps" API surface.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ npm install open-mcp-app
11
+ ```
12
+
13
+ ## Usage
14
+
15
+ ```js
16
+ const { createMcpAppsClient } = require('open-mcp-app');
17
+
18
+ const client = createMcpAppsClient({
19
+ baseUrl: 'https://api.example.com',
20
+ });
21
+
22
+ async function main() {
23
+ const apps = await client.listApps({});
24
+ console.log(apps);
25
+ }
26
+
27
+ main().catch((err) => {
28
+ console.error(err);
29
+ process.exitCode = 1;
30
+ });
31
+ ```
32
+
33
+ ## License
34
+
35
+ MIT
36
+
package/index.js ADDED
@@ -0,0 +1,363 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Ensures a value is a non-empty string so that the SDK fails fast with a
5
+ * consistent error message. This keeps downstream network errors from masking
6
+ * configuration mistakes and makes SDK usage easier to debug.
7
+ *
8
+ * @param {Object} args
9
+ * @param {unknown} args.value
10
+ * @param {string} args.name
11
+ * @returns {string}
12
+ */
13
+ const assertNonEmptyString = ({ value, name } = {}) => {
14
+ if (typeof value !== 'string' || value.trim().length === 0) {
15
+ throw new McpAppsSdkError({
16
+ message: `${name} must be a non-empty string.`,
17
+ code: 'MCP_APPS_INVALID_ARGUMENT',
18
+ });
19
+ }
20
+
21
+ return value;
22
+ };
23
+
24
+ /**
25
+ * Normalizes a base URL by removing trailing slashes so that URL construction is
26
+ * predictable. This avoids subtle double-slash URLs when callers pass a base
27
+ * URL like "https://api.example.com/".
28
+ *
29
+ * @param {Object} args
30
+ * @param {string} args.baseUrl
31
+ * @returns {string}
32
+ */
33
+ const normalizeBaseUrl = ({ baseUrl } = {}) => {
34
+ const url = assertNonEmptyString({ value: baseUrl, name: 'baseUrl' });
35
+ return url.replace(/\/+$/, '');
36
+ };
37
+
38
+ /**
39
+ * Builds a request URL from a base URL, a path, and an optional query object.
40
+ * Centralizing URL construction ensures consistent encoding rules across the
41
+ * SDK and reduces the chance of introducing subtle request bugs.
42
+ *
43
+ * @param {Object} args
44
+ * @param {string} args.baseUrl
45
+ * @param {string} args.path
46
+ * @param {Object<string, string | number | boolean | null | undefined>} [args.query]
47
+ * @returns {string}
48
+ */
49
+ const buildUrl = ({ baseUrl, path, query } = {}) => {
50
+ const normalizedBaseUrl = normalizeBaseUrl({ baseUrl });
51
+ const normalizedPath = assertNonEmptyString({ value: path, name: 'path' }).startsWith('/')
52
+ ? path
53
+ : `/${path}`;
54
+
55
+ const url = new URL(`${normalizedBaseUrl}${normalizedPath}`);
56
+
57
+ if (query && typeof query === 'object') {
58
+ for (const [key, value] of Object.entries(query)) {
59
+ if (value === undefined || value === null) continue;
60
+ url.searchParams.set(key, String(value));
61
+ }
62
+ }
63
+
64
+ return url.toString();
65
+ };
66
+
67
+ /**
68
+ * A base error type for the SDK so that callers can reliably distinguish
69
+ * expected SDK failures from other runtime errors.
70
+ */
71
+ class McpAppsSdkError extends Error {
72
+ /**
73
+ * Creates a new SDK error instance.
74
+ *
75
+ * @param {Object} args
76
+ * @param {string} args.message
77
+ * @param {string} [args.code]
78
+ * @param {unknown} [args.cause]
79
+ */
80
+ constructor({ message, code, cause } = {}) {
81
+ super(message);
82
+ this.name = 'McpAppsSdkError';
83
+ this.code = code || 'MCP_APPS_SDK_ERROR';
84
+
85
+ if (cause !== undefined) {
86
+ this.cause = cause;
87
+ }
88
+ }
89
+ }
90
+
91
+ /**
92
+ * An error thrown when an HTTP request completes but returns an unsuccessful
93
+ * status code. Surfacing the URL, method, status, and response body makes it
94
+ * straightforward to debug API failures without requiring extra logging.
95
+ */
96
+ class McpAppsRequestError extends McpAppsSdkError {
97
+ /**
98
+ * Creates a request error for a failed HTTP response.
99
+ *
100
+ * @param {Object} args
101
+ * @param {string} args.message
102
+ * @param {string} args.method
103
+ * @param {string} args.url
104
+ * @param {number} args.status
105
+ * @param {unknown} [args.body]
106
+ */
107
+ constructor({ message, method, url, status, body } = {}) {
108
+ super({ message, code: 'MCP_APPS_REQUEST_FAILED' });
109
+ this.name = 'McpAppsRequestError';
110
+ this.method = method;
111
+ this.url = url;
112
+ this.status = status;
113
+ this.body = body;
114
+ }
115
+ }
116
+
117
+ /**
118
+ * Creates a new `McpAppsClient` instance.
119
+ *
120
+ * This factory exists so consumers can create clients without `new`, which is
121
+ * a common ergonomic pattern for SDKs and helps keep usage consistent across
122
+ * codebases.
123
+ *
124
+ * @param {Object} args
125
+ * @param {string} args.baseUrl
126
+ * @param {string} [args.apiKey]
127
+ * @param {(input: string, init?: Object) => Promise<Response>} [args.fetchFn]
128
+ * @param {string} [args.userAgent]
129
+ * @returns {McpAppsClient}
130
+ */
131
+ const createMcpAppsClient = ({ baseUrl, apiKey, fetchFn, userAgent } = {}) =>
132
+ new McpAppsClient({ baseUrl, apiKey, fetchFn, userAgent });
133
+
134
+ /**
135
+ * A small, opinionated client that demonstrates the shape of an open MCP Apps SDK.
136
+ *
137
+ * The class stays intentionally lightweight: it provides a generic `request`
138
+ * primitive plus a few convenience methods that map to typical discovery and
139
+ * invocation workflows. The goal is to provide stable call signatures and
140
+ * error shapes even while the backing API evolves.
141
+ */
142
+ class McpAppsClient {
143
+ /**
144
+ * Creates a new client instance.
145
+ *
146
+ * @param {Object} args
147
+ * @param {string} args.baseUrl
148
+ * @param {string} [args.apiKey]
149
+ * @param {(input: string, init?: Object) => Promise<Response>} [args.fetchFn]
150
+ * @param {string} [args.userAgent]
151
+ */
152
+ constructor({ baseUrl, apiKey, fetchFn, userAgent } = {}) {
153
+ this._baseUrl = normalizeBaseUrl({ baseUrl });
154
+ this._apiKey = apiKey;
155
+ this._fetchFn = fetchFn;
156
+ this._userAgent = userAgent;
157
+ }
158
+
159
+ /**
160
+ * Updates client configuration at runtime.
161
+ *
162
+ * This exists so applications can construct a client early and later inject
163
+ * credentials or a custom fetch implementation without needing to pass the
164
+ * client instance through additional layers.
165
+ *
166
+ * @param {Object} args
167
+ * @param {string} [args.baseUrl]
168
+ * @param {string} [args.apiKey]
169
+ * @param {(input: string, init?: Object) => Promise<Response>} [args.fetchFn]
170
+ * @param {string} [args.userAgent]
171
+ * @returns {void}
172
+ */
173
+ configure = ({ baseUrl, apiKey, fetchFn, userAgent } = {}) => {
174
+ if (baseUrl !== undefined) this._baseUrl = normalizeBaseUrl({ baseUrl });
175
+ if (apiKey !== undefined) this._apiKey = apiKey;
176
+ if (fetchFn !== undefined) this._fetchFn = fetchFn;
177
+ if (userAgent !== undefined) this._userAgent = userAgent;
178
+ };
179
+
180
+ /**
181
+ * Issues an HTTP request and returns the parsed response body.
182
+ *
183
+ * The method is designed to be the lowest-level primitive in this SDK:
184
+ * higher-level helpers call into `request` so that retry, logging, custom
185
+ * headers, and transport behaviors can be added in one place.
186
+ *
187
+ * @param {Object} args
188
+ * @param {string} [args.method]
189
+ * @param {string} args.path
190
+ * @param {Object<string, string | number | boolean | null | undefined>} [args.query]
191
+ * @param {Object<string, string>} [args.headers]
192
+ * @param {unknown} [args.body]
193
+ * @param {AbortSignal} [args.signal]
194
+ * @returns {Promise<unknown>}
195
+ */
196
+ request = async ({ method, path, query, headers, body, signal } = {}) => {
197
+ const url = buildUrl({ baseUrl: this._baseUrl, path, query });
198
+
199
+ const fetchFn = this._fetchFn || globalThis.fetch;
200
+ if (typeof fetchFn !== 'function') {
201
+ throw new McpAppsSdkError({
202
+ message:
203
+ 'No fetch implementation is available. Provide fetchFn or use Node.js 18+ where fetch is built in.',
204
+ code: 'MCP_APPS_MISSING_FETCH',
205
+ });
206
+ }
207
+
208
+ const normalizedMethod = (method || 'GET').toUpperCase();
209
+
210
+ const requestHeaders = {
211
+ ...(headers || {}),
212
+ };
213
+
214
+ if (this._userAgent && !requestHeaders['user-agent'] && !requestHeaders['User-Agent']) {
215
+ requestHeaders['user-agent'] = this._userAgent;
216
+ }
217
+
218
+ if (this._apiKey && !requestHeaders.authorization && !requestHeaders.Authorization) {
219
+ requestHeaders.authorization = `Bearer ${this._apiKey}`;
220
+ }
221
+
222
+ /** @type {Object} */
223
+ const init = { method: normalizedMethod, headers: requestHeaders, signal };
224
+
225
+ if (body !== undefined) {
226
+ if (!requestHeaders['content-type'] && !requestHeaders['Content-Type']) {
227
+ requestHeaders['content-type'] = 'application/json';
228
+ }
229
+
230
+ init.body =
231
+ typeof body === 'string' || Buffer.isBuffer(body) ? body : JSON.stringify(body);
232
+ }
233
+
234
+ const response = await fetchFn(url, init);
235
+
236
+ const contentType = response.headers?.get?.('content-type') || '';
237
+ const isJson = contentType.toLowerCase().includes('application/json');
238
+
239
+ const responseBody = isJson
240
+ ? await response.json().catch(() => undefined)
241
+ : await response.text();
242
+
243
+ if (!response.ok) {
244
+ throw new McpAppsRequestError({
245
+ message: `Request failed with status ${response.status}.`,
246
+ method: normalizedMethod,
247
+ url,
248
+ status: response.status,
249
+ body: responseBody,
250
+ });
251
+ }
252
+
253
+ return responseBody;
254
+ };
255
+
256
+ /**
257
+ * Lists apps accessible to the current principal from the "open apps" surface.
258
+ *
259
+ * This method exists as a convenience helper for discovery flows. Routing
260
+ * through a dedicated open path keeps the intention clear for callers and
261
+ * allows backend policies to evolve independently from non-open app surfaces.
262
+ *
263
+ * @param {Object} args
264
+ * @param {AbortSignal} [args.signal]
265
+ * @returns {Promise<unknown>}
266
+ */
267
+ listApps = async ({ signal } = {}) =>
268
+ this.request({ method: 'GET', path: '/open/apps', signal });
269
+
270
+ /**
271
+ * Fetches a single open app by id.
272
+ *
273
+ * Explicitly requiring `appId` keeps the call signature self-documenting and
274
+ * avoids ambiguous positional parameters.
275
+ *
276
+ * @param {Object} args
277
+ * @param {string} args.appId
278
+ * @param {AbortSignal} [args.signal]
279
+ * @returns {Promise<unknown>}
280
+ */
281
+ getApp = async ({ appId, signal } = {}) => {
282
+ const id = assertNonEmptyString({ value: appId, name: 'appId' });
283
+ return this.request({ method: 'GET', path: `/open/apps/${encodeURIComponent(id)}`, signal });
284
+ };
285
+
286
+ /**
287
+ * Fetches an open app's manifest.
288
+ *
289
+ * Manifests are commonly used to describe capabilities (tools, input schema,
290
+ * output schema) without requiring execution. Exposing this as a separate
291
+ * call keeps discovery lightweight and avoids overloading `getApp`.
292
+ *
293
+ * @param {Object} args
294
+ * @param {string} args.appId
295
+ * @param {AbortSignal} [args.signal]
296
+ * @returns {Promise<unknown>}
297
+ */
298
+ getAppManifest = async ({ appId, signal } = {}) => {
299
+ const id = assertNonEmptyString({ value: appId, name: 'appId' });
300
+ return this.request({
301
+ method: 'GET',
302
+ path: `/open/apps/${encodeURIComponent(id)}/manifest`,
303
+ signal,
304
+ });
305
+ };
306
+
307
+ /**
308
+ * Runs an open app with an input payload.
309
+ *
310
+ * Many MCP-style "apps" are orchestrations that accept structured input and
311
+ * return structured output. This method provides a stable entry point for
312
+ * that interaction while keeping transport details inside `request`.
313
+ *
314
+ * @param {Object} args
315
+ * @param {string} args.appId
316
+ * @param {unknown} [args.input]
317
+ * @param {AbortSignal} [args.signal]
318
+ * @returns {Promise<unknown>}
319
+ */
320
+ runApp = async ({ appId, input, signal } = {}) => {
321
+ const id = assertNonEmptyString({ value: appId, name: 'appId' });
322
+ return this.request({
323
+ method: 'POST',
324
+ path: `/open/apps/${encodeURIComponent(id)}/run`,
325
+ body: { input },
326
+ signal,
327
+ });
328
+ };
329
+
330
+ /**
331
+ * Invokes a tool exposed by an open app.
332
+ *
333
+ * Tools are a core MCP primitive. This method intentionally accepts `args`
334
+ * as an object so that tool invocation stays forward-compatible when tool
335
+ * schemas evolve.
336
+ *
337
+ * @param {Object} args
338
+ * @param {string} args.appId
339
+ * @param {string} args.toolName
340
+ * @param {Object<string, unknown>} [args.args]
341
+ * @param {AbortSignal} [args.signal]
342
+ * @returns {Promise<unknown>}
343
+ */
344
+ invokeTool = async ({ appId, toolName, args, signal } = {}) => {
345
+ const id = assertNonEmptyString({ value: appId, name: 'appId' });
346
+ const tool = assertNonEmptyString({ value: toolName, name: 'toolName' });
347
+
348
+ return this.request({
349
+ method: 'POST',
350
+ path: `/open/apps/${encodeURIComponent(id)}/tools/${encodeURIComponent(tool)}`,
351
+ body: { args: args || {} },
352
+ signal,
353
+ });
354
+ };
355
+ }
356
+
357
+ module.exports = {
358
+ McpAppsSdkError,
359
+ McpAppsRequestError,
360
+ McpAppsClient,
361
+ createMcpAppsClient,
362
+ };
363
+
package/package.json ADDED
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "open-mcp-app",
3
+ "version": "0.1.0",
4
+ "description": "A small Node.js SDK skeleton for working with open MCP apps.",
5
+ "main": "index.js",
6
+ "exports": {
7
+ ".": "./index.js"
8
+ },
9
+ "files": [
10
+ "index.js",
11
+ "README.md",
12
+ "LICENSE"
13
+ ],
14
+ "scripts": {
15
+ "test": "node -e \"require('./index.js')\"",
16
+ "prepublishOnly": "npm test"
17
+ },
18
+ "keywords": [
19
+ "mcp",
20
+ "model-context-protocol",
21
+ "sdk",
22
+ "apps",
23
+ "open"
24
+ ],
25
+ "license": "MIT",
26
+ "engines": {
27
+ "node": ">=18"
28
+ },
29
+ "publishConfig": {
30
+ "access": "public"
31
+ }
32
+ }