@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.
Files changed (30) hide show
  1. package/README.md +51 -0
  2. package/dist/createServer/createServer.d.ts +11 -0
  3. package/dist/createServer/createServer.js +54 -0
  4. package/dist/errors/ManifestLoadError/ManifestLoadError.d.ts +10 -0
  5. package/dist/errors/ManifestLoadError/ManifestLoadError.js +14 -0
  6. package/dist/getDetails/getDetails.d.ts +15 -0
  7. package/dist/getDetails/getDetails.js +5 -0
  8. package/dist/index.d.ts +2 -0
  9. package/dist/index.js +20 -0
  10. package/dist/listComponents/listComponents.d.ts +9 -0
  11. package/dist/listComponents/listComponents.js +5 -0
  12. package/dist/loadManifest/loadManifest.d.ts +21 -0
  13. package/dist/loadManifest/loadManifest.js +26 -0
  14. package/dist/manifest-types.d.ts +18 -0
  15. package/dist/manifest-types.js +1 -0
  16. package/dist/parseManifest/parseManifest.d.ts +10 -0
  17. package/dist/parseManifest/parseManifest.js +23 -0
  18. package/dist/parseManifest/parseRawComponent/parseRawComponent.d.ts +16 -0
  19. package/dist/parseManifest/parseRawComponent/parseRawComponent.js +47 -0
  20. package/dist/parseManifest/parseRawProp/parseRawProp.d.ts +16 -0
  21. package/dist/parseManifest/parseRawProp/parseRawProp.js +20 -0
  22. package/dist/parseManifest/parseVariantValues/parseVariantValues.d.ts +17 -0
  23. package/dist/parseManifest/parseVariantValues/parseVariantValues.js +25 -0
  24. package/dist/redactSource/redactSource.d.ts +13 -0
  25. package/dist/redactSource/redactSource.js +22 -0
  26. package/dist/sanitizeName/sanitizeName.d.ts +15 -0
  27. package/dist/sanitizeName/sanitizeName.js +13 -0
  28. package/dist/searchComponents/searchComponents.d.ts +19 -0
  29. package/dist/searchComponents/searchComponents.js +21 -0
  30. 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;
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Finds a component by name (case-insensitive). Returns null when not found.
3
+ * @notInstrumented
4
+ */
5
+ export const getDetails = ({ manifest, name, }) => manifest.components.find((c) => c.name.toLowerCase() === name.toLowerCase()) ?? null;
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
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,5 @@
1
+ /**
2
+ * Returns a lightweight summary (name + description) for every component in the manifest.
3
+ * @notInstrumented
4
+ */
5
+ export const listComponents = (m) => m.components.map((c) => ({ description: c.description ?? '', name: c.name }));
@@ -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
- { "name": "@dynamic-labs-sdk/droplet-mcp", "version": "0.0.0" }
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
+ }