@contractspec/bundle.library 3.8.4 → 3.8.5
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/.turbo/turbo-build.log +126 -112
- package/CHANGELOG.md +6 -0
- package/dist/application/index.js +806 -131
- package/dist/application/mcp/cliMcp.js +21 -2
- package/dist/application/mcp/common.js +21 -2
- package/dist/application/mcp/common.test.d.ts +1 -0
- package/dist/application/mcp/contractsMcp.js +21 -2
- package/dist/application/mcp/docsMcp.catalog.d.ts +2 -0
- package/dist/application/mcp/docsMcp.catalog.js +382 -0
- package/dist/application/mcp/docsMcp.d.ts +5 -1
- package/dist/application/mcp/docsMcp.data.d.ts +85 -0
- package/dist/application/mcp/docsMcp.data.js +148 -0
- package/dist/application/mcp/docsMcp.js +776 -101
- package/dist/application/mcp/docsMcp.prompts.d.ts +3 -0
- package/dist/application/mcp/docsMcp.prompts.js +522 -0
- package/dist/application/mcp/docsMcp.reference.d.ts +24 -0
- package/dist/application/mcp/docsMcp.reference.js +236 -0
- package/dist/application/mcp/docsMcp.resources.d.ts +3 -0
- package/dist/application/mcp/docsMcp.resources.js +520 -0
- package/dist/application/mcp/docsMcp.test.d.ts +1 -0
- package/dist/application/mcp/docsMcp.tools.d.ts +3 -0
- package/dist/application/mcp/docsMcp.tools.js +519 -0
- package/dist/application/mcp/index.js +806 -131
- package/dist/application/mcp/internalMcp.js +21 -2
- package/dist/application/mcp/normalizeMcpRequest.d.ts +1 -0
- package/dist/application/mcp/normalizeMcpRequest.js +22 -0
- package/dist/application/mcp/providerRankingMcp.js +21 -2
- package/dist/features/index.js +15 -15
- package/dist/index.js +171 -171
- package/dist/node/application/index.js +806 -131
- package/dist/node/application/mcp/cliMcp.js +21 -2
- package/dist/node/application/mcp/common.js +21 -2
- package/dist/node/application/mcp/contractsMcp.js +21 -2
- package/dist/node/application/mcp/docsMcp.catalog.js +381 -0
- package/dist/node/application/mcp/docsMcp.data.js +147 -0
- package/dist/node/application/mcp/docsMcp.js +776 -101
- package/dist/node/application/mcp/docsMcp.prompts.js +521 -0
- package/dist/node/application/mcp/docsMcp.reference.js +235 -0
- package/dist/node/application/mcp/docsMcp.resources.js +519 -0
- package/dist/node/application/mcp/docsMcp.tools.js +518 -0
- package/dist/node/application/mcp/index.js +806 -131
- package/dist/node/application/mcp/internalMcp.js +21 -2
- package/dist/node/application/mcp/normalizeMcpRequest.js +21 -0
- package/dist/node/application/mcp/providerRankingMcp.js +21 -2
- package/dist/node/features/index.js +15 -15
- package/dist/node/index.js +171 -171
- package/dist/node/presentation/features/hooks/index.js +12 -12
- package/dist/node/presentation/features/hooks/useContractsRegistry.js +12 -12
- package/dist/node/presentation/features/index.js +12 -12
- package/dist/node/presentation/features/organisms/FeatureDataViewsList.js +12 -12
- package/dist/node/presentation/features/organisms/FeatureEventsList.js +12 -12
- package/dist/node/presentation/features/organisms/FeatureFormsList.js +12 -12
- package/dist/node/presentation/features/organisms/FeaturePresentationsList.js +12 -12
- package/dist/node/presentation/features/organisms/index.js +12 -12
- package/dist/node/presentation/features/templates/FeatureDataViewsTemplate/FeatureDataViewsTemplate.js +12 -12
- package/dist/node/presentation/features/templates/FeatureDataViewsTemplate/index.js +12 -12
- package/dist/node/presentation/features/templates/FeatureEventsTemplate/FeatureEventsTemplate.js +12 -12
- package/dist/node/presentation/features/templates/FeatureEventsTemplate/index.js +12 -12
- package/dist/node/presentation/features/templates/FeatureFormsTemplate/FeatureFormsTemplate.js +12 -12
- package/dist/node/presentation/features/templates/FeatureFormsTemplate/index.js +12 -12
- package/dist/node/presentation/features/templates/FeaturePresentationsTemplate/FeaturePresentationsTemplate.js +12 -12
- package/dist/node/presentation/features/templates/FeaturePresentationsTemplate/index.js +12 -12
- package/dist/presentation/features/hooks/index.js +12 -12
- package/dist/presentation/features/hooks/useContractsRegistry.js +12 -12
- package/dist/presentation/features/index.js +12 -12
- package/dist/presentation/features/organisms/FeatureDataViewsList.js +12 -12
- package/dist/presentation/features/organisms/FeatureEventsList.js +12 -12
- package/dist/presentation/features/organisms/FeatureFormsList.js +12 -12
- package/dist/presentation/features/organisms/FeaturePresentationsList.js +12 -12
- package/dist/presentation/features/organisms/index.js +12 -12
- package/dist/presentation/features/templates/FeatureDataViewsTemplate/FeatureDataViewsTemplate.js +12 -12
- package/dist/presentation/features/templates/FeatureDataViewsTemplate/index.js +12 -12
- package/dist/presentation/features/templates/FeatureEventsTemplate/FeatureEventsTemplate.js +12 -12
- package/dist/presentation/features/templates/FeatureEventsTemplate/index.js +12 -12
- package/dist/presentation/features/templates/FeatureFormsTemplate/FeatureFormsTemplate.js +12 -12
- package/dist/presentation/features/templates/FeatureFormsTemplate/index.js +12 -12
- package/dist/presentation/features/templates/FeaturePresentationsTemplate/FeaturePresentationsTemplate.js +12 -12
- package/dist/presentation/features/templates/FeaturePresentationsTemplate/index.js +12 -12
- package/package.json +85 -1
- package/src/application/mcp/common.test.ts +64 -0
- package/src/application/mcp/common.ts +5 -2
- package/src/application/mcp/docsMcp.catalog.ts +2 -0
- package/src/application/mcp/docsMcp.data.ts +196 -0
- package/src/application/mcp/docsMcp.prompts.ts +165 -0
- package/src/application/mcp/docsMcp.reference.ts +152 -0
- package/src/application/mcp/docsMcp.resources.ts +194 -0
- package/src/application/mcp/docsMcp.test.ts +148 -0
- package/src/application/mcp/docsMcp.tools.ts +183 -0
- package/src/application/mcp/docsMcp.ts +13 -177
- package/src/application/mcp/normalizeMcpRequest.ts +30 -0
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { definePrompt, PromptRegistry } from '@contractspec/lib.contracts-spec';
|
|
2
|
+
import type { DocPresentationRoute } from '@contractspec/lib.contracts-spec/docs';
|
|
3
|
+
import z from 'zod';
|
|
4
|
+
import { searchDocs } from './docsMcp.data';
|
|
5
|
+
import { resolveContractReference } from './docsMcp.reference';
|
|
6
|
+
|
|
7
|
+
const DOC_OWNERS = ['@contractspec'];
|
|
8
|
+
const DOC_TAGS = ['docs', 'mcp'];
|
|
9
|
+
|
|
10
|
+
export function buildDocPrompts(routes: DocPresentationRoute[]) {
|
|
11
|
+
const prompts = new PromptRegistry();
|
|
12
|
+
|
|
13
|
+
prompts.register(
|
|
14
|
+
definePrompt({
|
|
15
|
+
meta: {
|
|
16
|
+
key: 'docs.navigator',
|
|
17
|
+
version: '1.0.0',
|
|
18
|
+
title: 'Find relevant ContractSpec docs',
|
|
19
|
+
description:
|
|
20
|
+
'Guide agents to search, filter, and open the right ContractSpec docs.',
|
|
21
|
+
tags: DOC_TAGS,
|
|
22
|
+
stability: 'beta',
|
|
23
|
+
owners: DOC_OWNERS,
|
|
24
|
+
},
|
|
25
|
+
args: [
|
|
26
|
+
{
|
|
27
|
+
name: 'topic',
|
|
28
|
+
description: 'Goal or subject to search for.',
|
|
29
|
+
required: false,
|
|
30
|
+
schema: z.string().optional(),
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
name: 'kind',
|
|
34
|
+
description: 'Optional doc kind filter.',
|
|
35
|
+
required: false,
|
|
36
|
+
schema: z.string().optional(),
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
name: 'tag',
|
|
40
|
+
description: 'Optional tag filter.',
|
|
41
|
+
required: false,
|
|
42
|
+
schema: z.string().optional(),
|
|
43
|
+
},
|
|
44
|
+
],
|
|
45
|
+
input: z.object({
|
|
46
|
+
topic: z.string().optional(),
|
|
47
|
+
kind: z.string().optional(),
|
|
48
|
+
tag: z.string().optional(),
|
|
49
|
+
}),
|
|
50
|
+
render: async ({ topic, kind, tag }) => {
|
|
51
|
+
const matches = searchDocs(routes, {
|
|
52
|
+
query: topic,
|
|
53
|
+
kind,
|
|
54
|
+
tag,
|
|
55
|
+
limit: 3,
|
|
56
|
+
});
|
|
57
|
+
const suggestedDocs = matches.docs.length
|
|
58
|
+
? matches.docs
|
|
59
|
+
.map((doc) => `- ${doc.title} (${doc.id}) -> ${doc.route}`)
|
|
60
|
+
.join('\n')
|
|
61
|
+
: '- No direct pre-match. Use docs_list_facets-v1_0_0 to browse tags and kinds.';
|
|
62
|
+
|
|
63
|
+
return [
|
|
64
|
+
{
|
|
65
|
+
type: 'text' as const,
|
|
66
|
+
text: [
|
|
67
|
+
'Use docs_search-v1_0_0 first, then read docs://doc/{id} for the strongest matches.',
|
|
68
|
+
'Use docs_resolve_route-v1_0_0 when the user already gives you a docs URL or route.',
|
|
69
|
+
'Use docs_list_facets-v1_0_0 or docs://facets to browse the docs taxonomy before guessing.',
|
|
70
|
+
topic ? `Topic: ${topic}` : '',
|
|
71
|
+
kind ? `Kind: ${kind}` : '',
|
|
72
|
+
tag ? `Tag: ${tag}` : '',
|
|
73
|
+
'Suggested starting docs:',
|
|
74
|
+
suggestedDocs,
|
|
75
|
+
]
|
|
76
|
+
.filter(Boolean)
|
|
77
|
+
.join('\n'),
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
type: 'resource' as const,
|
|
81
|
+
uri: 'docs://index',
|
|
82
|
+
title: 'DocBlocks index',
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
type: 'resource' as const,
|
|
86
|
+
uri: 'docs://facets',
|
|
87
|
+
title: 'Docs facets',
|
|
88
|
+
},
|
|
89
|
+
];
|
|
90
|
+
},
|
|
91
|
+
})
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
prompts.register(
|
|
95
|
+
definePrompt({
|
|
96
|
+
meta: {
|
|
97
|
+
key: 'docs.reference.guide',
|
|
98
|
+
version: '1.0.0',
|
|
99
|
+
title: 'Resolve a ContractSpec reference',
|
|
100
|
+
description:
|
|
101
|
+
'Guide agents to fetch the canonical reference payload for a ContractSpec surface.',
|
|
102
|
+
tags: DOC_TAGS,
|
|
103
|
+
stability: 'beta',
|
|
104
|
+
owners: DOC_OWNERS,
|
|
105
|
+
},
|
|
106
|
+
args: [
|
|
107
|
+
{
|
|
108
|
+
name: 'key',
|
|
109
|
+
description: 'ContractSpec key to resolve.',
|
|
110
|
+
required: true,
|
|
111
|
+
schema: z.string(),
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
name: 'version',
|
|
115
|
+
description: 'Optional version override.',
|
|
116
|
+
required: false,
|
|
117
|
+
schema: z.string().optional(),
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
name: 'type',
|
|
121
|
+
description:
|
|
122
|
+
'Optional surface type: command, query, form, data-view, presentation, event.',
|
|
123
|
+
required: false,
|
|
124
|
+
schema: z.string().optional(),
|
|
125
|
+
},
|
|
126
|
+
],
|
|
127
|
+
input: z.object({
|
|
128
|
+
key: z.string(),
|
|
129
|
+
version: z.string().optional(),
|
|
130
|
+
type: z.string().optional(),
|
|
131
|
+
}),
|
|
132
|
+
render: async ({ key, version, type }) => {
|
|
133
|
+
const reference = resolveContractReference({
|
|
134
|
+
key,
|
|
135
|
+
version,
|
|
136
|
+
type,
|
|
137
|
+
includeSchema: true,
|
|
138
|
+
}).reference;
|
|
139
|
+
|
|
140
|
+
return [
|
|
141
|
+
{
|
|
142
|
+
type: 'text' as const,
|
|
143
|
+
text: [
|
|
144
|
+
'Use docs_contract_reference-v1_0_0 when you need the canonical docs payload for a ContractSpec surface.',
|
|
145
|
+
'Use docs_get-v1_0_0 only when you already know the exact DocBlock id and need raw markdown.',
|
|
146
|
+
`Resolved key: ${reference.key}`,
|
|
147
|
+
`Resolved type: ${reference.type}`,
|
|
148
|
+
reference.route ? `Docs route: ${reference.route}` : '',
|
|
149
|
+
`Resource URI: docs://contract-reference/${encodeURIComponent(key)}`,
|
|
150
|
+
]
|
|
151
|
+
.filter(Boolean)
|
|
152
|
+
.join('\n'),
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
type: 'resource' as const,
|
|
156
|
+
uri: `docs://contract-reference/${encodeURIComponent(key)}`,
|
|
157
|
+
title: 'Contract reference',
|
|
158
|
+
},
|
|
159
|
+
];
|
|
160
|
+
},
|
|
161
|
+
})
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
return prompts;
|
|
165
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AnyEventSpec,
|
|
3
|
+
AnyOperationSpec,
|
|
4
|
+
FormSpec,
|
|
5
|
+
} from '@contractspec/lib.contracts-spec';
|
|
6
|
+
import type { DataViewSpec } from '@contractspec/lib.contracts-spec/data-views';
|
|
7
|
+
import { defaultDocRegistry } from '@contractspec/lib.contracts-spec/docs';
|
|
8
|
+
import type { PresentationSpec } from '@contractspec/lib.contracts-spec/presentations';
|
|
9
|
+
import {
|
|
10
|
+
resolveDataViewSpec,
|
|
11
|
+
resolveEventSpec,
|
|
12
|
+
resolveFormSpec,
|
|
13
|
+
resolveOperationSpec,
|
|
14
|
+
resolvePresentationSpec,
|
|
15
|
+
resolveSerializedDataViewSpec,
|
|
16
|
+
resolveSerializedEventSpec,
|
|
17
|
+
resolveSerializedFormSpec,
|
|
18
|
+
resolveSerializedOperationSpec,
|
|
19
|
+
resolveSerializedPresentationSpec,
|
|
20
|
+
} from '../../features/contracts-registry';
|
|
21
|
+
|
|
22
|
+
interface ContractReferenceArgs {
|
|
23
|
+
key: string;
|
|
24
|
+
version?: string;
|
|
25
|
+
type?: string;
|
|
26
|
+
format?: string;
|
|
27
|
+
includeSchema?: boolean;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function normalizeText(value: string | undefined | null) {
|
|
31
|
+
return value?.trim().toLowerCase() ?? '';
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function routeFromDocIds(docIds: string[] | undefined) {
|
|
35
|
+
for (const docId of docIds ?? []) {
|
|
36
|
+
const doc = defaultDocRegistry.get(docId);
|
|
37
|
+
if (doc) return doc.route;
|
|
38
|
+
}
|
|
39
|
+
return undefined;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function toReference(
|
|
43
|
+
spec:
|
|
44
|
+
| AnyOperationSpec
|
|
45
|
+
| AnyEventSpec
|
|
46
|
+
| PresentationSpec
|
|
47
|
+
| DataViewSpec
|
|
48
|
+
| FormSpec,
|
|
49
|
+
type: string,
|
|
50
|
+
schema: unknown,
|
|
51
|
+
policy?: unknown
|
|
52
|
+
) {
|
|
53
|
+
const title = spec.meta.title ?? spec.meta.key;
|
|
54
|
+
const route = routeFromDocIds(spec.meta.docId);
|
|
55
|
+
const description = spec.meta.description;
|
|
56
|
+
|
|
57
|
+
return {
|
|
58
|
+
key: spec.meta.key,
|
|
59
|
+
version: spec.meta.version,
|
|
60
|
+
type,
|
|
61
|
+
title,
|
|
62
|
+
description,
|
|
63
|
+
markdown: [
|
|
64
|
+
`# ${title}`,
|
|
65
|
+
`- Key: ${spec.meta.key}`,
|
|
66
|
+
`- Type: ${type}`,
|
|
67
|
+
`- Version: ${spec.meta.version}`,
|
|
68
|
+
route ? `- Docs route: ${route}` : '',
|
|
69
|
+
description ? `\n${description}` : '',
|
|
70
|
+
]
|
|
71
|
+
.filter(Boolean)
|
|
72
|
+
.join('\n'),
|
|
73
|
+
...(route ? { route } : {}),
|
|
74
|
+
...(schema ? { schema } : {}),
|
|
75
|
+
...(policy ? { policy } : {}),
|
|
76
|
+
tags: spec.meta.tags ?? [],
|
|
77
|
+
owners: spec.meta.owners ?? [],
|
|
78
|
+
stability: spec.meta.stability,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function resolveContractReference(args: ContractReferenceArgs) {
|
|
83
|
+
const includeSchema = args.includeSchema ?? false;
|
|
84
|
+
const requestedType = normalizeText(args.type);
|
|
85
|
+
|
|
86
|
+
const operation = resolveOperationSpec(args.key, args.version);
|
|
87
|
+
if (
|
|
88
|
+
operation &&
|
|
89
|
+
(!requestedType ||
|
|
90
|
+
requestedType === 'operation' ||
|
|
91
|
+
requestedType === operation.meta.kind)
|
|
92
|
+
) {
|
|
93
|
+
return {
|
|
94
|
+
reference: toReference(
|
|
95
|
+
operation,
|
|
96
|
+
operation.meta.kind,
|
|
97
|
+
includeSchema
|
|
98
|
+
? resolveSerializedOperationSpec(args.key, args.version)
|
|
99
|
+
: undefined,
|
|
100
|
+
operation.policy
|
|
101
|
+
),
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const resolvers = [
|
|
106
|
+
{
|
|
107
|
+
type: 'data-view',
|
|
108
|
+
spec: resolveDataViewSpec(args.key, args.version),
|
|
109
|
+
schema: includeSchema
|
|
110
|
+
? resolveSerializedDataViewSpec(args.key, args.version)
|
|
111
|
+
: undefined,
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
type: 'form',
|
|
115
|
+
spec: resolveFormSpec(args.key, args.version),
|
|
116
|
+
schema: includeSchema
|
|
117
|
+
? resolveSerializedFormSpec(args.key, args.version)
|
|
118
|
+
: undefined,
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
type: 'presentation',
|
|
122
|
+
spec: resolvePresentationSpec(args.key, args.version),
|
|
123
|
+
schema: includeSchema
|
|
124
|
+
? resolveSerializedPresentationSpec(args.key, args.version)
|
|
125
|
+
: undefined,
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
type: 'event',
|
|
129
|
+
spec: resolveEventSpec(args.key, args.version),
|
|
130
|
+
schema: includeSchema
|
|
131
|
+
? resolveSerializedEventSpec(args.key, args.version)
|
|
132
|
+
: undefined,
|
|
133
|
+
},
|
|
134
|
+
] as const;
|
|
135
|
+
|
|
136
|
+
for (const candidate of resolvers) {
|
|
137
|
+
if (
|
|
138
|
+
candidate.spec &&
|
|
139
|
+
(!requestedType || requestedType === candidate.type)
|
|
140
|
+
) {
|
|
141
|
+
return {
|
|
142
|
+
reference: toReference(
|
|
143
|
+
candidate.spec,
|
|
144
|
+
candidate.type,
|
|
145
|
+
candidate.schema
|
|
146
|
+
),
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
throw new Error(`Contract reference not found: ${args.key}`);
|
|
152
|
+
}
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import {
|
|
2
|
+
defineResourceTemplate,
|
|
3
|
+
ResourceRegistry,
|
|
4
|
+
} from '@contractspec/lib.contracts-spec';
|
|
5
|
+
import type { DocPresentationRoute } from '@contractspec/lib.contracts-spec/docs';
|
|
6
|
+
import z from 'zod';
|
|
7
|
+
import {
|
|
8
|
+
getDocById,
|
|
9
|
+
getDocByRoute,
|
|
10
|
+
listDocFacets,
|
|
11
|
+
searchDocs,
|
|
12
|
+
} from './docsMcp.data';
|
|
13
|
+
import { resolveContractReference } from './docsMcp.reference';
|
|
14
|
+
|
|
15
|
+
const DOC_TAGS = ['docs', 'mcp'];
|
|
16
|
+
|
|
17
|
+
export function buildDocResources(routes: DocPresentationRoute[]) {
|
|
18
|
+
const resources = new ResourceRegistry();
|
|
19
|
+
const readDocIndex = (input: {
|
|
20
|
+
query?: string;
|
|
21
|
+
tag?: string;
|
|
22
|
+
kind?: string;
|
|
23
|
+
visibility?: string;
|
|
24
|
+
limit?: number;
|
|
25
|
+
offset?: number;
|
|
26
|
+
}) => searchDocs(routes, input);
|
|
27
|
+
|
|
28
|
+
resources.register(
|
|
29
|
+
defineResourceTemplate({
|
|
30
|
+
meta: {
|
|
31
|
+
uriTemplate: 'docs://index',
|
|
32
|
+
title: 'DocBlocks index',
|
|
33
|
+
description: 'Default ContractSpec docs index resource.',
|
|
34
|
+
mimeType: 'application/json',
|
|
35
|
+
tags: DOC_TAGS,
|
|
36
|
+
},
|
|
37
|
+
input: z.object({}),
|
|
38
|
+
resolve: async () => ({
|
|
39
|
+
uri: 'docs://index',
|
|
40
|
+
mimeType: 'application/json',
|
|
41
|
+
data: JSON.stringify(readDocIndex({}), null, 2),
|
|
42
|
+
}),
|
|
43
|
+
})
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
resources.register(
|
|
47
|
+
defineResourceTemplate({
|
|
48
|
+
meta: {
|
|
49
|
+
uriTemplate: 'docs://index{?query,tag,kind,visibility,limit,offset}',
|
|
50
|
+
title: 'DocBlocks index',
|
|
51
|
+
description:
|
|
52
|
+
'Search and paginate ContractSpec docs by query, tag, kind, or visibility.',
|
|
53
|
+
mimeType: 'application/json',
|
|
54
|
+
tags: DOC_TAGS,
|
|
55
|
+
},
|
|
56
|
+
input: z.object({
|
|
57
|
+
query: z.string().optional(),
|
|
58
|
+
tag: z.string().optional(),
|
|
59
|
+
kind: z.string().optional(),
|
|
60
|
+
visibility: z.string().optional(),
|
|
61
|
+
limit: z.coerce.number().optional(),
|
|
62
|
+
offset: z.coerce.number().optional(),
|
|
63
|
+
}),
|
|
64
|
+
resolve: async (input) => ({
|
|
65
|
+
uri: 'docs://index',
|
|
66
|
+
mimeType: 'application/json',
|
|
67
|
+
data: JSON.stringify(readDocIndex(input), null, 2),
|
|
68
|
+
}),
|
|
69
|
+
})
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
resources.register(
|
|
73
|
+
defineResourceTemplate({
|
|
74
|
+
meta: {
|
|
75
|
+
uriTemplate: 'docs://list',
|
|
76
|
+
title: 'DocBlocks index (legacy alias)',
|
|
77
|
+
description: 'Compatibility alias for the docs index resource.',
|
|
78
|
+
mimeType: 'application/json',
|
|
79
|
+
tags: DOC_TAGS,
|
|
80
|
+
},
|
|
81
|
+
input: z.object({}),
|
|
82
|
+
resolve: async () => ({
|
|
83
|
+
uri: 'docs://list',
|
|
84
|
+
mimeType: 'application/json',
|
|
85
|
+
data: JSON.stringify(readDocIndex({}), null, 2),
|
|
86
|
+
}),
|
|
87
|
+
})
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
resources.register(
|
|
91
|
+
defineResourceTemplate({
|
|
92
|
+
meta: {
|
|
93
|
+
uriTemplate: 'docs://doc/{id}',
|
|
94
|
+
title: 'Doc markdown',
|
|
95
|
+
description: 'Fetch a single DocBlock body by id as markdown.',
|
|
96
|
+
mimeType: 'text/markdown',
|
|
97
|
+
tags: DOC_TAGS,
|
|
98
|
+
},
|
|
99
|
+
input: z.object({ id: z.string() }),
|
|
100
|
+
resolve: async ({ id }) => {
|
|
101
|
+
const found = getDocById(id);
|
|
102
|
+
if (!found) {
|
|
103
|
+
return {
|
|
104
|
+
uri: `docs://doc/${encodeURIComponent(id)}`,
|
|
105
|
+
mimeType: 'text/plain',
|
|
106
|
+
data: `DocBlock not found: ${id}`,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
uri: `docs://doc/${encodeURIComponent(id)}`,
|
|
112
|
+
mimeType: 'text/markdown',
|
|
113
|
+
data: found.content,
|
|
114
|
+
};
|
|
115
|
+
},
|
|
116
|
+
})
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
resources.register(
|
|
120
|
+
defineResourceTemplate({
|
|
121
|
+
meta: {
|
|
122
|
+
uriTemplate: 'docs://route/{routePath}',
|
|
123
|
+
title: 'Doc by route',
|
|
124
|
+
description:
|
|
125
|
+
'Resolve a docs route to the matching DocBlock summary and body.',
|
|
126
|
+
mimeType: 'application/json',
|
|
127
|
+
tags: DOC_TAGS,
|
|
128
|
+
},
|
|
129
|
+
input: z.object({ routePath: z.string() }),
|
|
130
|
+
resolve: async ({ routePath }) => ({
|
|
131
|
+
uri: `docs://route/${encodeURIComponent(routePath)}`,
|
|
132
|
+
mimeType: 'application/json',
|
|
133
|
+
data: JSON.stringify(
|
|
134
|
+
getDocByRoute(routes, routePath) ?? {
|
|
135
|
+
error: 'not_found',
|
|
136
|
+
route: routePath,
|
|
137
|
+
},
|
|
138
|
+
null,
|
|
139
|
+
2
|
|
140
|
+
),
|
|
141
|
+
}),
|
|
142
|
+
})
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
resources.register(
|
|
146
|
+
defineResourceTemplate({
|
|
147
|
+
meta: {
|
|
148
|
+
uriTemplate: 'docs://facets',
|
|
149
|
+
title: 'Docs facets',
|
|
150
|
+
description:
|
|
151
|
+
'Counts of available tags, kinds, and visibilities across docs.',
|
|
152
|
+
mimeType: 'application/json',
|
|
153
|
+
tags: DOC_TAGS,
|
|
154
|
+
},
|
|
155
|
+
input: z.object({}),
|
|
156
|
+
resolve: async () => ({
|
|
157
|
+
uri: 'docs://facets',
|
|
158
|
+
mimeType: 'application/json',
|
|
159
|
+
data: JSON.stringify(listDocFacets(routes), null, 2),
|
|
160
|
+
}),
|
|
161
|
+
})
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
resources.register(
|
|
165
|
+
defineResourceTemplate({
|
|
166
|
+
meta: {
|
|
167
|
+
uriTemplate:
|
|
168
|
+
'docs://contract-reference/{key}{?version,type,includeSchema}',
|
|
169
|
+
title: 'Contract reference',
|
|
170
|
+
description:
|
|
171
|
+
'Resolve a ContractSpec surface into a docs-ready reference payload.',
|
|
172
|
+
mimeType: 'application/json',
|
|
173
|
+
tags: DOC_TAGS,
|
|
174
|
+
},
|
|
175
|
+
input: z.object({
|
|
176
|
+
key: z.string(),
|
|
177
|
+
version: z.string().optional(),
|
|
178
|
+
type: z.string().optional(),
|
|
179
|
+
includeSchema: z.coerce.boolean().optional(),
|
|
180
|
+
}),
|
|
181
|
+
resolve: async ({ key, version, type, includeSchema }) => ({
|
|
182
|
+
uri: `docs://contract-reference/${encodeURIComponent(key)}`,
|
|
183
|
+
mimeType: 'application/json',
|
|
184
|
+
data: JSON.stringify(
|
|
185
|
+
resolveContractReference({ key, version, type, includeSchema }),
|
|
186
|
+
null,
|
|
187
|
+
2
|
|
188
|
+
),
|
|
189
|
+
}),
|
|
190
|
+
})
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
return resources;
|
|
194
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { describe, expect, it } from 'bun:test';
|
|
2
|
+
import { Elysia } from 'elysia';
|
|
3
|
+
import { createDocsMcpHandler } from './docsMcp';
|
|
4
|
+
|
|
5
|
+
function createTestApp() {
|
|
6
|
+
return new Elysia().use(createDocsMcpHandler('/mcp/docs'));
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
async function mcpRequest(
|
|
10
|
+
method: string,
|
|
11
|
+
params: Record<string, unknown> = {}
|
|
12
|
+
) {
|
|
13
|
+
return createTestApp().handle(
|
|
14
|
+
new Request('http://localhost/mcp/docs', {
|
|
15
|
+
method: 'POST',
|
|
16
|
+
headers: {
|
|
17
|
+
accept: 'application/json',
|
|
18
|
+
'content-type': 'application/json',
|
|
19
|
+
},
|
|
20
|
+
body: JSON.stringify({
|
|
21
|
+
jsonrpc: '2.0',
|
|
22
|
+
id: 1,
|
|
23
|
+
method,
|
|
24
|
+
params,
|
|
25
|
+
}),
|
|
26
|
+
})
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async function initialize() {
|
|
31
|
+
const response = await mcpRequest('initialize', {
|
|
32
|
+
protocolVersion: '2024-11-05',
|
|
33
|
+
capabilities: {},
|
|
34
|
+
clientInfo: { name: 'docs-mcp-test', version: '1.0.0' },
|
|
35
|
+
});
|
|
36
|
+
expect(response.status).toBe(200);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
describe('docs MCP handler', () => {
|
|
40
|
+
it('exposes curated tools, prompts, and resources', async () => {
|
|
41
|
+
await initialize();
|
|
42
|
+
|
|
43
|
+
const toolsResponse = await mcpRequest('tools/list');
|
|
44
|
+
const toolsBody = await toolsResponse.json();
|
|
45
|
+
const toolNames = toolsBody.result.tools.map(
|
|
46
|
+
(tool: { name: string }) => tool.name
|
|
47
|
+
);
|
|
48
|
+
expect(toolNames).toContain('docs_search-v1_0_0');
|
|
49
|
+
expect(toolNames).toContain('docs_get-v1_0_0');
|
|
50
|
+
expect(toolNames).toContain('docs_resolve_route-v1_0_0');
|
|
51
|
+
expect(toolNames).toContain('docs_contract_reference-v1_0_0');
|
|
52
|
+
expect(toolNames).toContain('docs_list_facets-v1_0_0');
|
|
53
|
+
|
|
54
|
+
const promptsResponse = await mcpRequest('prompts/list');
|
|
55
|
+
const promptsBody = await promptsResponse.json();
|
|
56
|
+
const promptNames = promptsBody.result.prompts.map(
|
|
57
|
+
(prompt: { name: string }) => prompt.name
|
|
58
|
+
);
|
|
59
|
+
expect(promptNames).toContain('docs_navigator');
|
|
60
|
+
expect(promptNames).toContain('docs_reference_guide');
|
|
61
|
+
|
|
62
|
+
const listedResourcesResponse = await mcpRequest('resources/list');
|
|
63
|
+
const listedResourcesBody = await listedResourcesResponse.json();
|
|
64
|
+
const listedResources = listedResourcesBody.result.resources ?? [];
|
|
65
|
+
const listedResourceUris = listedResources.map(
|
|
66
|
+
(resource: { uri?: string; uriTemplate?: string }) =>
|
|
67
|
+
resource.uriTemplate ?? resource.uri
|
|
68
|
+
);
|
|
69
|
+
expect(listedResourceUris).toContain('docs://index');
|
|
70
|
+
expect(listedResourceUris).toContain('docs://list');
|
|
71
|
+
expect(listedResourceUris).toContain('docs://facets');
|
|
72
|
+
|
|
73
|
+
const templatesResponse = await mcpRequest('resources/templates/list');
|
|
74
|
+
const templatesBody = await templatesResponse.json();
|
|
75
|
+
const templates = templatesBody.result.resourceTemplates ?? [];
|
|
76
|
+
const templateUris = templates.map(
|
|
77
|
+
(resource: { uri?: string; uriTemplate?: string }) =>
|
|
78
|
+
resource.uriTemplate ?? resource.uri
|
|
79
|
+
);
|
|
80
|
+
expect(templateUris).toContain(
|
|
81
|
+
'docs://index{?query,tag,kind,visibility,limit,offset}'
|
|
82
|
+
);
|
|
83
|
+
expect(templateUris).toContain(
|
|
84
|
+
'docs://contract-reference/{key}{?version,type,includeSchema}'
|
|
85
|
+
);
|
|
86
|
+
expect(
|
|
87
|
+
listedResourceUris.some((uri: string) =>
|
|
88
|
+
uri.startsWith('presentation://')
|
|
89
|
+
)
|
|
90
|
+
).toBeFalse();
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('supports paginated docs search and contract reference lookup', async () => {
|
|
94
|
+
await initialize();
|
|
95
|
+
|
|
96
|
+
const searchResponse = await mcpRequest('tools/call', {
|
|
97
|
+
name: 'docs_search-v1_0_0',
|
|
98
|
+
arguments: {
|
|
99
|
+
query: 'docs',
|
|
100
|
+
kind: 'how',
|
|
101
|
+
limit: 1,
|
|
102
|
+
},
|
|
103
|
+
});
|
|
104
|
+
const searchBody = await searchResponse.json();
|
|
105
|
+
const searchResult = JSON.parse(searchBody.result.content[0].text);
|
|
106
|
+
expect(searchResult.docs).toBeArray();
|
|
107
|
+
expect(searchResult.docs.length).toBe(1);
|
|
108
|
+
expect(searchResult.total).toBeGreaterThan(1);
|
|
109
|
+
expect(searchResult.nextOffset).toBe(1);
|
|
110
|
+
|
|
111
|
+
const referenceResponse = await mcpRequest('tools/call', {
|
|
112
|
+
name: 'docs_contract_reference-v1_0_0',
|
|
113
|
+
arguments: {
|
|
114
|
+
key: 'docs.generate',
|
|
115
|
+
type: 'command',
|
|
116
|
+
includeSchema: true,
|
|
117
|
+
},
|
|
118
|
+
});
|
|
119
|
+
const referenceBody = await referenceResponse.json();
|
|
120
|
+
const referenceResult = JSON.parse(referenceBody.result.content[0].text);
|
|
121
|
+
expect(referenceResult.reference.type).toBe('command');
|
|
122
|
+
expect(referenceResult.reference.route).toBe('/docs/tech/docs/generator');
|
|
123
|
+
expect(referenceResult.reference.schema.meta.key).toBe('docs.generate');
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('keeps markdown doc resources and richer prompts usable', async () => {
|
|
127
|
+
await initialize();
|
|
128
|
+
|
|
129
|
+
const docResponse = await mcpRequest('resources/read', {
|
|
130
|
+
uri: 'docs://doc/docs.tech.docs-generator',
|
|
131
|
+
});
|
|
132
|
+
const docBody = await docResponse.json();
|
|
133
|
+
expect(docBody.result.contents[0].text).toContain('# Docs generator');
|
|
134
|
+
|
|
135
|
+
const promptResponse = await mcpRequest('prompts/get', {
|
|
136
|
+
name: 'docs_reference_guide',
|
|
137
|
+
arguments: {
|
|
138
|
+
key: 'docs.generate',
|
|
139
|
+
type: 'command',
|
|
140
|
+
},
|
|
141
|
+
});
|
|
142
|
+
const promptBody = await promptResponse.json();
|
|
143
|
+
const text = promptBody.result.messages[0].content.text;
|
|
144
|
+
expect(text).toContain('docs_contract_reference-v1_0_0');
|
|
145
|
+
expect(text).toContain('docs.generate');
|
|
146
|
+
expect(text).toContain('docs://contract-reference/docs.generate');
|
|
147
|
+
});
|
|
148
|
+
});
|