@dynamic-labs-sdk/droplet-mcp 0.0.0 → 1.16.1
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/README.md +51 -0
- package/dist/createServer/createServer.d.ts +11 -0
- package/dist/createServer/createServer.js +54 -0
- package/dist/errors/ManifestLoadError/ManifestLoadError.d.ts +10 -0
- package/dist/errors/ManifestLoadError/ManifestLoadError.js +14 -0
- package/dist/getDetails/getDetails.d.ts +15 -0
- package/dist/getDetails/getDetails.js +5 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +20 -0
- package/dist/listComponents/listComponents.d.ts +9 -0
- package/dist/listComponents/listComponents.js +5 -0
- package/dist/loadManifest/loadManifest.d.ts +21 -0
- package/dist/loadManifest/loadManifest.js +26 -0
- package/dist/manifest-types.d.ts +18 -0
- package/dist/manifest-types.js +1 -0
- package/dist/parseManifest/parseManifest.d.ts +10 -0
- package/dist/parseManifest/parseManifest.js +23 -0
- package/dist/parseManifest/parseRawComponent/parseRawComponent.d.ts +16 -0
- package/dist/parseManifest/parseRawComponent/parseRawComponent.js +47 -0
- package/dist/parseManifest/parseRawProp/parseRawProp.d.ts +16 -0
- package/dist/parseManifest/parseRawProp/parseRawProp.js +20 -0
- package/dist/parseManifest/parseVariantValues/parseVariantValues.d.ts +17 -0
- package/dist/parseManifest/parseVariantValues/parseVariantValues.js +25 -0
- package/dist/redactSource/redactSource.d.ts +13 -0
- package/dist/redactSource/redactSource.js +22 -0
- package/dist/sanitizeName/sanitizeName.d.ts +15 -0
- package/dist/sanitizeName/sanitizeName.js +13 -0
- package/dist/searchComponents/searchComponents.d.ts +19 -0
- package/dist/searchComponents/searchComponents.js +21 -0
- package/package.json +29 -1
package/README.md
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# @dynamic-labs-sdk/droplet-mcp
|
|
2
|
+
|
|
3
|
+
An MCP (Model Context Protocol) server that exposes Droplet's component manifest to coding agents. It provides three tools — `list-components`, `get-details`, and `search-components` — so AI coding assistants can discover and reason about the available UI components without reading raw source files.
|
|
4
|
+
|
|
5
|
+
## Usage
|
|
6
|
+
|
|
7
|
+
### Build
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pnpm build
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
### Run
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
node dist/index.js
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
By default the server reads `./components.json`. Set `DROPLET_MANIFEST` to override:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
# Local file
|
|
23
|
+
DROPLET_MANIFEST=./manifests/components.json node dist/index.js
|
|
24
|
+
|
|
25
|
+
# Remote URL (Storybook-generated manifest)
|
|
26
|
+
DROPLET_MANIFEST=https://your-storybook.example.com/manifests/components.json node dist/index.js
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### Environment variables
|
|
30
|
+
|
|
31
|
+
| Variable | Default | Description |
|
|
32
|
+
| ------------------ | ------------------- | --------------------------------------------------------------------------- |
|
|
33
|
+
| `DROPLET_MANIFEST` | `./components.json` | Path or `http(s)://` URL to the Storybook-generated `manifests/components.json` |
|
|
34
|
+
|
|
35
|
+
### MCP registration (`.mcp.json`)
|
|
36
|
+
|
|
37
|
+
Add the server to your project's `.mcp.json` so your coding agent picks it up automatically:
|
|
38
|
+
|
|
39
|
+
```json
|
|
40
|
+
{
|
|
41
|
+
"mcpServers": {
|
|
42
|
+
"droplet": {
|
|
43
|
+
"command": "node",
|
|
44
|
+
"args": ["./node_modules/@dynamic-labs-sdk/droplet-mcp/dist/index.js"],
|
|
45
|
+
"env": {
|
|
46
|
+
"DROPLET_MANIFEST": "./manifests/components.json"
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
```
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
import type { ComponentsManifest } from '../manifest-types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Creates an MCP server pre-configured with three droplet knowledge tools:
|
|
5
|
+
* `list-components`, `get-details`, and `search-components`.
|
|
6
|
+
*
|
|
7
|
+
* @param manifest - The parsed components manifest used to answer tool queries.
|
|
8
|
+
* @returns A configured McpServer instance ready to be connected to a transport.
|
|
9
|
+
* @notInstrumented
|
|
10
|
+
*/
|
|
11
|
+
export declare const createServer: (manifest: ComponentsManifest) => McpServer;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { getDetails } from '../getDetails/getDetails.js';
|
|
4
|
+
import { listComponents } from '../listComponents/listComponents.js';
|
|
5
|
+
import { sanitizeName } from '../sanitizeName/sanitizeName.js';
|
|
6
|
+
import { searchComponents } from '../searchComponents/searchComponents.js';
|
|
7
|
+
/**
|
|
8
|
+
* Creates an MCP server pre-configured with three droplet knowledge tools:
|
|
9
|
+
* `list-components`, `get-details`, and `search-components`.
|
|
10
|
+
*
|
|
11
|
+
* @param manifest - The parsed components manifest used to answer tool queries.
|
|
12
|
+
* @returns A configured McpServer instance ready to be connected to a transport.
|
|
13
|
+
* @notInstrumented
|
|
14
|
+
*/
|
|
15
|
+
export const createServer = (manifest) => {
|
|
16
|
+
const server = new McpServer({ name: 'droplet-mcp', version: '0.1.0' });
|
|
17
|
+
server.registerTool('list-components', {
|
|
18
|
+
description: 'List all droplet components with a one-line description. Call this before building UI to see what exists.',
|
|
19
|
+
}, async () => ({
|
|
20
|
+
content: [
|
|
21
|
+
{ text: JSON.stringify(listComponents(manifest), null, 2), type: 'text' },
|
|
22
|
+
],
|
|
23
|
+
}));
|
|
24
|
+
server.registerTool('get-details', {
|
|
25
|
+
description: 'Get the exact props, variants, and example for one droplet component. Use this instead of guessing the API from memory.',
|
|
26
|
+
inputSchema: { name: z.string().max(100).describe('Component name, e.g. "Button"') },
|
|
27
|
+
}, async ({ name }) => {
|
|
28
|
+
const details = getDetails({ manifest, name });
|
|
29
|
+
// Sanitize name before echoing to prevent control character injection in error messages
|
|
30
|
+
const safeName = sanitizeName({ name });
|
|
31
|
+
return {
|
|
32
|
+
content: [
|
|
33
|
+
{
|
|
34
|
+
text: details
|
|
35
|
+
? JSON.stringify(details, null, 2)
|
|
36
|
+
: `No droplet component named "${safeName}". Call list-components for valid names.`,
|
|
37
|
+
type: 'text',
|
|
38
|
+
},
|
|
39
|
+
],
|
|
40
|
+
};
|
|
41
|
+
});
|
|
42
|
+
server.registerTool('search-components', {
|
|
43
|
+
description: 'Search droplet components by keyword (name, description, variant, or prop name), e.g. "dialog", "toggle", "empty state".',
|
|
44
|
+
inputSchema: { query: z.string().max(200).describe('Keyword to search for') },
|
|
45
|
+
}, async ({ query }) => ({
|
|
46
|
+
content: [
|
|
47
|
+
{
|
|
48
|
+
text: JSON.stringify(searchComponents({ manifest, query }), null, 2),
|
|
49
|
+
type: 'text',
|
|
50
|
+
},
|
|
51
|
+
],
|
|
52
|
+
}));
|
|
53
|
+
return server;
|
|
54
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Error thrown when a droplet manifest cannot be loaded — e.g. the source uses
|
|
3
|
+
* plain HTTP, an HTTPS fetch returns a non-OK status, or the local file read fails.
|
|
4
|
+
*
|
|
5
|
+
* Using a dedicated subclass (instead of bare `new Error`) keeps load failures
|
|
6
|
+
* distinguishable from other runtime errors at the CLI boundary in `index.ts`.
|
|
7
|
+
*/
|
|
8
|
+
export declare class ManifestLoadError extends Error {
|
|
9
|
+
constructor(message: string);
|
|
10
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Error thrown when a droplet manifest cannot be loaded — e.g. the source uses
|
|
3
|
+
* plain HTTP, an HTTPS fetch returns a non-OK status, or the local file read fails.
|
|
4
|
+
*
|
|
5
|
+
* Using a dedicated subclass (instead of bare `new Error`) keeps load failures
|
|
6
|
+
* distinguishable from other runtime errors at the CLI boundary in `index.ts`.
|
|
7
|
+
*/
|
|
8
|
+
export class ManifestLoadError extends Error {
|
|
9
|
+
// eslint-disable-next-line custom-rules/require-single-object-param
|
|
10
|
+
constructor(message) {
|
|
11
|
+
super(message);
|
|
12
|
+
this.name = 'ManifestLoadError';
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { ComponentsManifest, ManifestComponent } from '../manifest-types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Parameters for {@link getDetails}.
|
|
4
|
+
*/
|
|
5
|
+
export type GetDetailsParams = {
|
|
6
|
+
/** The parsed components manifest to search. */
|
|
7
|
+
manifest: ComponentsManifest;
|
|
8
|
+
/** Component name to look up (matched case-insensitively). */
|
|
9
|
+
name: string;
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* Finds a component by name (case-insensitive). Returns null when not found.
|
|
13
|
+
* @notInstrumented
|
|
14
|
+
*/
|
|
15
|
+
export declare const getDetails: ({ manifest, name, }: GetDetailsParams) => ManifestComponent | null;
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
3
|
+
import { loadManifest } from './loadManifest/loadManifest.js';
|
|
4
|
+
import { parseManifest } from './parseManifest/parseManifest.js';
|
|
5
|
+
import { createServer } from './createServer/createServer.js';
|
|
6
|
+
import { redactSource } from './redactSource/redactSource.js';
|
|
7
|
+
const source = process.env.DROPLET_MANIFEST ?? './components.json';
|
|
8
|
+
let raw;
|
|
9
|
+
try {
|
|
10
|
+
raw = await loadManifest({ source });
|
|
11
|
+
}
|
|
12
|
+
catch (err) {
|
|
13
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
14
|
+
// Redact credentials/tokens from URL before writing to stderr
|
|
15
|
+
const safeSource = redactSource({ source });
|
|
16
|
+
process.stderr.write(`Failed to load droplet manifest from "${safeSource}": ${message}\n`);
|
|
17
|
+
process.exit(1);
|
|
18
|
+
}
|
|
19
|
+
const server = createServer(parseManifest(raw));
|
|
20
|
+
await server.connect(new StdioServerTransport());
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { ComponentsManifest } from '../manifest-types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Returns a lightweight summary (name + description) for every component in the manifest.
|
|
4
|
+
* @notInstrumented
|
|
5
|
+
*/
|
|
6
|
+
export declare const listComponents: (m: ComponentsManifest) => {
|
|
7
|
+
description: string;
|
|
8
|
+
name: string;
|
|
9
|
+
}[];
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parameters for {@link loadManifest}.
|
|
3
|
+
*/
|
|
4
|
+
export type LoadManifestParams = {
|
|
5
|
+
/**
|
|
6
|
+
* Where to load the manifest from: an `https://` URL (fetched over TLS) or a
|
|
7
|
+
* local file path. Plain `http://` URLs are rejected.
|
|
8
|
+
*/
|
|
9
|
+
source: string;
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* Loads raw manifest JSON from a source string.
|
|
13
|
+
*
|
|
14
|
+
* - `https://` sources are fetched over TLS.
|
|
15
|
+
* - `http://` sources are rejected outright to prevent MITM attacks.
|
|
16
|
+
* - All other strings are treated as local file paths read via `readFileSync`.
|
|
17
|
+
*
|
|
18
|
+
* @throws {ManifestLoadError} When the source uses plain HTTP or the fetch fails.
|
|
19
|
+
* @notInstrumented
|
|
20
|
+
*/
|
|
21
|
+
export declare const loadManifest: ({ source, }: LoadManifestParams) => Promise<unknown>;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { ManifestLoadError } from '../errors/ManifestLoadError/ManifestLoadError.js';
|
|
3
|
+
/**
|
|
4
|
+
* Loads raw manifest JSON from a source string.
|
|
5
|
+
*
|
|
6
|
+
* - `https://` sources are fetched over TLS.
|
|
7
|
+
* - `http://` sources are rejected outright to prevent MITM attacks.
|
|
8
|
+
* - All other strings are treated as local file paths read via `readFileSync`.
|
|
9
|
+
*
|
|
10
|
+
* @throws {ManifestLoadError} When the source uses plain HTTP or the fetch fails.
|
|
11
|
+
* @notInstrumented
|
|
12
|
+
*/
|
|
13
|
+
export const loadManifest = async ({ source, }) => {
|
|
14
|
+
if (source.startsWith('https://')) {
|
|
15
|
+
const res = await fetch(source);
|
|
16
|
+
if (!res.ok) {
|
|
17
|
+
throw new ManifestLoadError(`HTTP ${res.status} ${res.statusText}`);
|
|
18
|
+
}
|
|
19
|
+
return res.json();
|
|
20
|
+
}
|
|
21
|
+
if (source.startsWith('http://')) {
|
|
22
|
+
// Refuse plain HTTP to prevent MITM attacks on the manifest
|
|
23
|
+
throw new ManifestLoadError('Refusing to load manifest over plain HTTP — use https://');
|
|
24
|
+
}
|
|
25
|
+
return JSON.parse(readFileSync(source, 'utf8'));
|
|
26
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export type ManifestProp = {
|
|
2
|
+
defaultValue?: string;
|
|
3
|
+
description?: string;
|
|
4
|
+
name: string;
|
|
5
|
+
required?: boolean;
|
|
6
|
+
type?: string;
|
|
7
|
+
};
|
|
8
|
+
export type ManifestComponent = {
|
|
9
|
+
description?: string;
|
|
10
|
+
examples?: string[];
|
|
11
|
+
name: string;
|
|
12
|
+
props?: ManifestProp[];
|
|
13
|
+
related?: string[];
|
|
14
|
+
variants?: Record<string, string[]>;
|
|
15
|
+
};
|
|
16
|
+
export type ComponentsManifest = {
|
|
17
|
+
components: ManifestComponent[];
|
|
18
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { ComponentsManifest } from '../manifest-types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Adapts a raw Storybook-derived manifest (as stored on disk) into the normalized
|
|
4
|
+
* `ComponentsManifest` shape consumed by the MCP query functions.
|
|
5
|
+
*
|
|
6
|
+
* This function is intentionally defensive: any falsy, null, or structurally unexpected
|
|
7
|
+
* input returns `{ components: [] }` rather than throwing.
|
|
8
|
+
* @notInstrumented
|
|
9
|
+
*/
|
|
10
|
+
export declare const parseManifest: (raw: unknown) => ComponentsManifest;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { parseRawComponent } from './parseRawComponent/parseRawComponent.js';
|
|
2
|
+
/**
|
|
3
|
+
* Adapts a raw Storybook-derived manifest (as stored on disk) into the normalized
|
|
4
|
+
* `ComponentsManifest` shape consumed by the MCP query functions.
|
|
5
|
+
*
|
|
6
|
+
* This function is intentionally defensive: any falsy, null, or structurally unexpected
|
|
7
|
+
* input returns `{ components: [] }` rather than throwing.
|
|
8
|
+
* @notInstrumented
|
|
9
|
+
*/
|
|
10
|
+
export const parseManifest = (raw) => {
|
|
11
|
+
if (raw == null || typeof raw !== 'object') {
|
|
12
|
+
return { components: [] };
|
|
13
|
+
}
|
|
14
|
+
const rawObj = raw;
|
|
15
|
+
const componentsDict = rawObj['components'];
|
|
16
|
+
if (componentsDict == null ||
|
|
17
|
+
typeof componentsDict !== 'object' ||
|
|
18
|
+
Array.isArray(componentsDict)) {
|
|
19
|
+
return { components: [] };
|
|
20
|
+
}
|
|
21
|
+
const components = Object.entries(componentsDict).map(([storyId, entry]) => parseRawComponent({ rawEntry: entry, storyId }));
|
|
22
|
+
return { components };
|
|
23
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { ManifestComponent } from '../../manifest-types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Parameters for {@link parseRawComponent}.
|
|
4
|
+
*/
|
|
5
|
+
export type ParseRawComponentParams = {
|
|
6
|
+
/** The raw component entry (one value from `raw.components`). */
|
|
7
|
+
rawEntry: unknown;
|
|
8
|
+
/** Storybook dict key used as the fallback component name. */
|
|
9
|
+
storyId: string;
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* Converts a single raw component entry (one value from `raw.components`) into a
|
|
13
|
+
* `ManifestComponent`. The storybook dict key (`storyId`) is used as fallback name.
|
|
14
|
+
* @notInstrumented
|
|
15
|
+
*/
|
|
16
|
+
export declare const parseRawComponent: ({ storyId, rawEntry, }: ParseRawComponentParams) => ManifestComponent;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { parseRawProp } from '../parseRawProp/parseRawProp.js';
|
|
2
|
+
import { parseVariantValues } from '../parseVariantValues/parseVariantValues.js';
|
|
3
|
+
/**
|
|
4
|
+
* Converts a single raw component entry (one value from `raw.components`) into a
|
|
5
|
+
* `ManifestComponent`. The storybook dict key (`storyId`) is used as fallback name.
|
|
6
|
+
* @notInstrumented
|
|
7
|
+
*/
|
|
8
|
+
export const parseRawComponent = ({ storyId, rawEntry, }) => {
|
|
9
|
+
const entry = rawEntry;
|
|
10
|
+
const name = typeof entry['name'] === 'string' ? entry['name'] : storyId;
|
|
11
|
+
const rdgt = entry['reactDocgenTypescript'];
|
|
12
|
+
const rawDescription = rdgt?.['description'];
|
|
13
|
+
const description = typeof rawDescription === 'string' && rawDescription.length > 0
|
|
14
|
+
? rawDescription
|
|
15
|
+
: undefined;
|
|
16
|
+
// Parse props from reactDocgenTypescript.props dict
|
|
17
|
+
const rawProps = (rdgt?.['props'] ?? {});
|
|
18
|
+
const props = Object.entries(rawProps).map(([key, val]) => parseRawProp({ propKey: key, rawProp: val }));
|
|
19
|
+
// Derive variant sets from props whose type is a union of quoted string literals
|
|
20
|
+
const variantEntries = [];
|
|
21
|
+
for (const prop of props) {
|
|
22
|
+
if (prop.type !== undefined) {
|
|
23
|
+
const values = parseVariantValues({ typeName: prop.type });
|
|
24
|
+
if (values !== undefined) {
|
|
25
|
+
variantEntries.push([prop.name, values]);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
// Only include variants when at least one prop yields a variant set
|
|
30
|
+
const variants = variantEntries.length > 0 ? Object.fromEntries(variantEntries) : undefined;
|
|
31
|
+
// Extract example code snippets from stories; filter to strings only
|
|
32
|
+
const rawStories = Array.isArray(entry['stories']) ? entry['stories'] : [];
|
|
33
|
+
const examples = rawStories
|
|
34
|
+
.map((s) => {
|
|
35
|
+
const story = s;
|
|
36
|
+
return story['snippet'];
|
|
37
|
+
})
|
|
38
|
+
.filter((snippet) => typeof snippet === 'string');
|
|
39
|
+
return {
|
|
40
|
+
description,
|
|
41
|
+
// examples should be undefined when empty, consistent with props/variants
|
|
42
|
+
examples: examples.length > 0 ? examples : undefined,
|
|
43
|
+
name,
|
|
44
|
+
props: props.length > 0 ? props : undefined,
|
|
45
|
+
variants,
|
|
46
|
+
};
|
|
47
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { ManifestProp } from '../../manifest-types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Parameters for {@link parseRawProp}.
|
|
4
|
+
*/
|
|
5
|
+
export type ParseRawPropParams = {
|
|
6
|
+
/** Canonical prop name used when the raw entry has no `name` field. */
|
|
7
|
+
propKey: string;
|
|
8
|
+
/** The raw prop entry from `reactDocgenTypescript.props`. */
|
|
9
|
+
rawProp: unknown;
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* Converts a single raw prop entry (from `reactDocgenTypescript.props`) into a `ManifestProp`.
|
|
13
|
+
* The prop key from the dict is used as the canonical name.
|
|
14
|
+
* @notInstrumented
|
|
15
|
+
*/
|
|
16
|
+
export declare const parseRawProp: ({ propKey, rawProp, }: ParseRawPropParams) => ManifestProp;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Converts a single raw prop entry (from `reactDocgenTypescript.props`) into a `ManifestProp`.
|
|
3
|
+
* The prop key from the dict is used as the canonical name.
|
|
4
|
+
* @notInstrumented
|
|
5
|
+
*/
|
|
6
|
+
export const parseRawProp = ({ propKey, rawProp, }) => {
|
|
7
|
+
const p = rawProp;
|
|
8
|
+
const name = typeof p['name'] === 'string' ? p['name'] : propKey;
|
|
9
|
+
const description = typeof p['description'] === 'string' && p['description'].length > 0
|
|
10
|
+
? p['description']
|
|
11
|
+
: undefined;
|
|
12
|
+
const typeObj = p['type'];
|
|
13
|
+
const typeName = typeof typeObj?.['name'] === 'string' ? typeObj['name'] : undefined;
|
|
14
|
+
const defaultValueObj = p['defaultValue'];
|
|
15
|
+
const defaultValue = defaultValueObj != null && typeof defaultValueObj['value'] === 'string'
|
|
16
|
+
? defaultValueObj['value']
|
|
17
|
+
: undefined;
|
|
18
|
+
const required = typeof p['required'] === 'boolean' ? p['required'] : undefined;
|
|
19
|
+
return { defaultValue, description, name, required, type: typeName };
|
|
20
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parameters for {@link parseVariantValues}.
|
|
3
|
+
*/
|
|
4
|
+
export type ParseVariantValuesParams = {
|
|
5
|
+
/** Raw prop type name, possibly a union like `"default" | "outline" | null`. */
|
|
6
|
+
typeName: string;
|
|
7
|
+
};
|
|
8
|
+
/**
|
|
9
|
+
* Parses a raw prop type name (which may be a union string like `"default" | "outline" | null`)
|
|
10
|
+
* into an array of string-literal values. Only quoted string members are kept;
|
|
11
|
+
* tokens like `null`, `undefined`, `boolean`, and `string` are discarded.
|
|
12
|
+
*
|
|
13
|
+
* Returns the array only when it has at least 2 members (i.e. a real variant set).
|
|
14
|
+
* Returns undefined otherwise.
|
|
15
|
+
* @notInstrumented
|
|
16
|
+
*/
|
|
17
|
+
export declare const parseVariantValues: ({ typeName, }: ParseVariantValuesParams) => string[] | undefined;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Quoted-string-literal pattern for detecting enum-like union types.
|
|
3
|
+
* Matches members like `"default"` and captures the inner string value.
|
|
4
|
+
*/
|
|
5
|
+
const QUOTED_LITERAL_RE = /^"([^"]+)"$/;
|
|
6
|
+
/**
|
|
7
|
+
* Parses a raw prop type name (which may be a union string like `"default" | "outline" | null`)
|
|
8
|
+
* into an array of string-literal values. Only quoted string members are kept;
|
|
9
|
+
* tokens like `null`, `undefined`, `boolean`, and `string` are discarded.
|
|
10
|
+
*
|
|
11
|
+
* Returns the array only when it has at least 2 members (i.e. a real variant set).
|
|
12
|
+
* Returns undefined otherwise.
|
|
13
|
+
* @notInstrumented
|
|
14
|
+
*/
|
|
15
|
+
export const parseVariantValues = ({ typeName, }) => {
|
|
16
|
+
const members = typeName.split('|').map((t) => t.trim());
|
|
17
|
+
const literals = [];
|
|
18
|
+
for (const member of members) {
|
|
19
|
+
const match = QUOTED_LITERAL_RE.exec(member);
|
|
20
|
+
if (match) {
|
|
21
|
+
literals.push(match[1]);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return literals.length >= 2 ? literals : undefined;
|
|
25
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parameters for {@link redactSource}.
|
|
3
|
+
*/
|
|
4
|
+
export type RedactSourceParams = {
|
|
5
|
+
/** The manifest source (URL or file path) to redact before logging. */
|
|
6
|
+
source: string;
|
|
7
|
+
};
|
|
8
|
+
/**
|
|
9
|
+
* Redacts credentials (userinfo + query string) from http(s) URLs before logging,
|
|
10
|
+
* so we never leak secrets in error output. File paths are returned as-is.
|
|
11
|
+
* @notInstrumented
|
|
12
|
+
*/
|
|
13
|
+
export declare const redactSource: ({ source }: RedactSourceParams) => string;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Redacts credentials (userinfo + query string) from http(s) URLs before logging,
|
|
3
|
+
* so we never leak secrets in error output. File paths are returned as-is.
|
|
4
|
+
* @notInstrumented
|
|
5
|
+
*/
|
|
6
|
+
export const redactSource = ({ source }) => {
|
|
7
|
+
if (source.startsWith('https://') || source.startsWith('http://')) {
|
|
8
|
+
try {
|
|
9
|
+
const u = new URL(source);
|
|
10
|
+
// Strip userinfo (user:pass@) and query string — they may carry tokens
|
|
11
|
+
u.username = '';
|
|
12
|
+
u.password = '';
|
|
13
|
+
u.search = '';
|
|
14
|
+
return u.toString();
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
// If URL parsing fails, return a generic placeholder
|
|
18
|
+
return '[invalid url]';
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return source;
|
|
22
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parameters for {@link sanitizeName}.
|
|
3
|
+
*/
|
|
4
|
+
export type SanitizeNameParams = {
|
|
5
|
+
/** Maximum length of the returned string. Defaults to 50. */
|
|
6
|
+
maxLen?: number;
|
|
7
|
+
/** The user-supplied name to sanitize. */
|
|
8
|
+
name: string;
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* Strips control characters and newlines from a string, then truncates to maxLen.
|
|
12
|
+
* Used to sanitize user-supplied component names before echoing them in error messages.
|
|
13
|
+
* @notInstrumented
|
|
14
|
+
*/
|
|
15
|
+
export declare const sanitizeName: ({ name, maxLen, }: SanitizeNameParams) => string;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Default maximum length applied when truncating sanitized names.
|
|
3
|
+
*/
|
|
4
|
+
const DEFAULT_MAX_LEN = 50;
|
|
5
|
+
/**
|
|
6
|
+
* Strips control characters and newlines from a string, then truncates to maxLen.
|
|
7
|
+
* Used to sanitize user-supplied component names before echoing them in error messages.
|
|
8
|
+
* @notInstrumented
|
|
9
|
+
*/
|
|
10
|
+
export const sanitizeName = ({ name, maxLen = DEFAULT_MAX_LEN, }) =>
|
|
11
|
+
// Intentionally strips control characters from the name
|
|
12
|
+
// eslint-disable-next-line no-control-regex
|
|
13
|
+
name.replace(/[\x00-\x1F\x7F]/g, '').slice(0, maxLen);
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { ComponentsManifest } from '../manifest-types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Parameters for {@link searchComponents}.
|
|
4
|
+
*/
|
|
5
|
+
export type SearchComponentsParams = {
|
|
6
|
+
/** The parsed components manifest to search. */
|
|
7
|
+
manifest: ComponentsManifest;
|
|
8
|
+
/** Keyword to match against name, description, variant values, and prop names. */
|
|
9
|
+
query: string;
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* Searches components by matching the query against name, description, variant values,
|
|
13
|
+
* and prop names. Returns an empty array when the query is blank.
|
|
14
|
+
* @notInstrumented
|
|
15
|
+
*/
|
|
16
|
+
export declare const searchComponents: ({ manifest, query }: SearchComponentsParams) => {
|
|
17
|
+
description: string;
|
|
18
|
+
name: string;
|
|
19
|
+
}[];
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Searches components by matching the query against name, description, variant values,
|
|
3
|
+
* and prop names. Returns an empty array when the query is blank.
|
|
4
|
+
* @notInstrumented
|
|
5
|
+
*/
|
|
6
|
+
export const searchComponents = ({ manifest, query }) => {
|
|
7
|
+
const q = query.trim().toLowerCase();
|
|
8
|
+
if (!q)
|
|
9
|
+
return [];
|
|
10
|
+
return manifest.components
|
|
11
|
+
.filter((c) => c.name.toLowerCase().includes(q) ||
|
|
12
|
+
(c.description?.toLowerCase().includes(q) ?? false) ||
|
|
13
|
+
(c.variants
|
|
14
|
+
? Object.values(c.variants)
|
|
15
|
+
.flat()
|
|
16
|
+
.some((v) => v.toLowerCase().includes(q))
|
|
17
|
+
: false) ||
|
|
18
|
+
// Also match if any prop name contains the query
|
|
19
|
+
(c.props ? c.props.some((p) => p.name.toLowerCase().includes(q)) : false))
|
|
20
|
+
.map((c) => ({ description: c.description ?? '', name: c.name }));
|
|
21
|
+
};
|
package/package.json
CHANGED
|
@@ -1 +1,29 @@
|
|
|
1
|
-
{
|
|
1
|
+
{
|
|
2
|
+
"name": "@dynamic-labs-sdk/droplet-mcp",
|
|
3
|
+
"version": "1.16.1",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"bin": {
|
|
6
|
+
"droplet-mcp": "./dist/index.js"
|
|
7
|
+
},
|
|
8
|
+
"main": "./dist/index.js",
|
|
9
|
+
"files": [
|
|
10
|
+
"dist"
|
|
11
|
+
],
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"@modelcontextprotocol/sdk": "1.29.0",
|
|
14
|
+
"zod": "4.0.5"
|
|
15
|
+
},
|
|
16
|
+
"devDependencies": {
|
|
17
|
+
"typescript": "~5.7.2",
|
|
18
|
+
"vitest": "4.1.6"
|
|
19
|
+
},
|
|
20
|
+
"scripts": {
|
|
21
|
+
"build": "tsc -p tsconfig.build.json",
|
|
22
|
+
"test": "vitest run",
|
|
23
|
+
"test:watch": "vitest",
|
|
24
|
+
"test-with-coverage": "vitest run --coverage",
|
|
25
|
+
"start": "node dist/index.js",
|
|
26
|
+
"lint": "eslint .",
|
|
27
|
+
"lint:fix": "eslint . --fix"
|
|
28
|
+
}
|
|
29
|
+
}
|