@contractspec/bundle.library 3.8.4 → 3.8.7
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 +56 -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 +108 -24
- 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,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
|
+
});
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import {
|
|
2
|
+
defineCommand,
|
|
3
|
+
defineSchemaModel,
|
|
4
|
+
installOp,
|
|
5
|
+
OperationSpecRegistry,
|
|
6
|
+
} from '@contractspec/lib.contracts-spec';
|
|
7
|
+
import type { DocPresentationRoute } from '@contractspec/lib.contracts-spec/docs';
|
|
8
|
+
import {
|
|
9
|
+
ContractReferenceInput,
|
|
10
|
+
ContractReferenceOutput,
|
|
11
|
+
DocsIndexInput,
|
|
12
|
+
DocsIndexOutput,
|
|
13
|
+
} from '@contractspec/lib.contracts-spec/docs';
|
|
14
|
+
import { ScalarTypeEnum } from '@contractspec/lib.schema';
|
|
15
|
+
import {
|
|
16
|
+
getDocById,
|
|
17
|
+
getDocByRoute,
|
|
18
|
+
listDocFacets,
|
|
19
|
+
searchDocs,
|
|
20
|
+
} from './docsMcp.data';
|
|
21
|
+
import { resolveContractReference } from './docsMcp.reference';
|
|
22
|
+
|
|
23
|
+
const DOC_OWNERS = ['@contractspec'];
|
|
24
|
+
const DOC_TAGS = ['docs', 'mcp'];
|
|
25
|
+
|
|
26
|
+
const DocsGetInput = defineSchemaModel({
|
|
27
|
+
name: 'DocsGetInput',
|
|
28
|
+
fields: {
|
|
29
|
+
id: { type: ScalarTypeEnum.String_unsecure(), isOptional: false },
|
|
30
|
+
},
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const DocsGetOutput = defineSchemaModel({
|
|
34
|
+
name: 'DocsGetOutput',
|
|
35
|
+
fields: {
|
|
36
|
+
doc: { type: ScalarTypeEnum.JSON(), isOptional: false },
|
|
37
|
+
content: { type: ScalarTypeEnum.String_unsecure(), isOptional: false },
|
|
38
|
+
},
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const DocsResolveRouteInput = defineSchemaModel({
|
|
42
|
+
name: 'DocsResolveRouteInput',
|
|
43
|
+
fields: {
|
|
44
|
+
route: { type: ScalarTypeEnum.String_unsecure(), isOptional: false },
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
const DocsResolveRouteOutput = defineSchemaModel({
|
|
49
|
+
name: 'DocsResolveRouteOutput',
|
|
50
|
+
fields: {
|
|
51
|
+
doc: { type: ScalarTypeEnum.JSON(), isOptional: false },
|
|
52
|
+
content: { type: ScalarTypeEnum.String_unsecure(), isOptional: false },
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
const DocsFacetsInput = defineSchemaModel({
|
|
57
|
+
name: 'DocsFacetsInput',
|
|
58
|
+
fields: {},
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
const DocsFacetsOutput = defineSchemaModel({
|
|
62
|
+
name: 'DocsFacetsOutput',
|
|
63
|
+
fields: {
|
|
64
|
+
facets: { type: ScalarTypeEnum.JSON(), isOptional: false },
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
export function buildDocOps(routes: DocPresentationRoute[]) {
|
|
69
|
+
const registry = new OperationSpecRegistry();
|
|
70
|
+
|
|
71
|
+
installOp(
|
|
72
|
+
registry,
|
|
73
|
+
defineCommand({
|
|
74
|
+
meta: {
|
|
75
|
+
key: 'docs.search',
|
|
76
|
+
version: '1.0.0',
|
|
77
|
+
stability: 'beta',
|
|
78
|
+
owners: DOC_OWNERS,
|
|
79
|
+
tags: DOC_TAGS,
|
|
80
|
+
description:
|
|
81
|
+
'Search ContractSpec docs by query, tag, kind, or visibility.',
|
|
82
|
+
goal: 'Find the most relevant DocBlocks without browsing the full corpus.',
|
|
83
|
+
context: 'Read-only docs MCP search surface.',
|
|
84
|
+
},
|
|
85
|
+
io: { input: DocsIndexInput, output: DocsIndexOutput },
|
|
86
|
+
policy: { auth: 'anonymous' },
|
|
87
|
+
transport: { mcp: { toolName: 'docs_search-v1_0_0' } },
|
|
88
|
+
}),
|
|
89
|
+
async (args) => searchDocs(routes, args)
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
installOp(
|
|
93
|
+
registry,
|
|
94
|
+
defineCommand({
|
|
95
|
+
meta: {
|
|
96
|
+
key: 'docs.get',
|
|
97
|
+
version: '1.0.0',
|
|
98
|
+
stability: 'beta',
|
|
99
|
+
owners: DOC_OWNERS,
|
|
100
|
+
tags: DOC_TAGS,
|
|
101
|
+
description: 'Read a single DocBlock by id.',
|
|
102
|
+
goal: 'Fetch the exact markdown content and metadata for a known doc id.',
|
|
103
|
+
context: 'Read-only docs MCP surface.',
|
|
104
|
+
},
|
|
105
|
+
io: { input: DocsGetInput, output: DocsGetOutput },
|
|
106
|
+
policy: { auth: 'anonymous' },
|
|
107
|
+
transport: { mcp: { toolName: 'docs_get-v1_0_0' } },
|
|
108
|
+
}),
|
|
109
|
+
async ({ id }) => {
|
|
110
|
+
const found = getDocById(id);
|
|
111
|
+
if (!found) throw new Error(`DocBlock not found: ${id}`);
|
|
112
|
+
return found;
|
|
113
|
+
}
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
installOp(
|
|
117
|
+
registry,
|
|
118
|
+
defineCommand({
|
|
119
|
+
meta: {
|
|
120
|
+
key: 'docs.resolveRoute',
|
|
121
|
+
version: '1.0.0',
|
|
122
|
+
stability: 'beta',
|
|
123
|
+
owners: DOC_OWNERS,
|
|
124
|
+
tags: DOC_TAGS,
|
|
125
|
+
description: 'Resolve a docs route to the matching DocBlock.',
|
|
126
|
+
goal: 'Turn a route or URL path into a canonical doc id and markdown body.',
|
|
127
|
+
context: 'Read-only docs MCP surface.',
|
|
128
|
+
},
|
|
129
|
+
io: { input: DocsResolveRouteInput, output: DocsResolveRouteOutput },
|
|
130
|
+
policy: { auth: 'anonymous' },
|
|
131
|
+
transport: { mcp: { toolName: 'docs_resolve_route-v1_0_0' } },
|
|
132
|
+
}),
|
|
133
|
+
async ({ route }) => {
|
|
134
|
+
const found = getDocByRoute(routes, route);
|
|
135
|
+
if (!found) throw new Error(`Doc route not found: ${route}`);
|
|
136
|
+
return found;
|
|
137
|
+
}
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
installOp(
|
|
141
|
+
registry,
|
|
142
|
+
defineCommand({
|
|
143
|
+
meta: {
|
|
144
|
+
key: 'docs.contract.lookup',
|
|
145
|
+
version: '1.0.0',
|
|
146
|
+
stability: 'beta',
|
|
147
|
+
owners: DOC_OWNERS,
|
|
148
|
+
tags: DOC_TAGS,
|
|
149
|
+
description:
|
|
150
|
+
'Resolve a ContractSpec surface into a docs-ready reference payload.',
|
|
151
|
+
goal: 'Get canonical docs metadata, route, and optional schema for a spec key.',
|
|
152
|
+
context: 'Read-only docs MCP surface.',
|
|
153
|
+
},
|
|
154
|
+
io: { input: ContractReferenceInput, output: ContractReferenceOutput },
|
|
155
|
+
policy: { auth: 'anonymous' },
|
|
156
|
+
transport: { mcp: { toolName: 'docs_contract_reference-v1_0_0' } },
|
|
157
|
+
}),
|
|
158
|
+
async (args) => resolveContractReference(args)
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
installOp(
|
|
162
|
+
registry,
|
|
163
|
+
defineCommand({
|
|
164
|
+
meta: {
|
|
165
|
+
key: 'docs.list.facets',
|
|
166
|
+
version: '1.0.0',
|
|
167
|
+
stability: 'beta',
|
|
168
|
+
owners: DOC_OWNERS,
|
|
169
|
+
tags: DOC_TAGS,
|
|
170
|
+
description:
|
|
171
|
+
'List docs taxonomy facets such as tags, kinds, and visibilities.',
|
|
172
|
+
goal: 'Help agents browse the docs corpus before making targeted reads.',
|
|
173
|
+
context: 'Read-only docs MCP surface.',
|
|
174
|
+
},
|
|
175
|
+
io: { input: DocsFacetsInput, output: DocsFacetsOutput },
|
|
176
|
+
policy: { auth: 'anonymous' },
|
|
177
|
+
transport: { mcp: { toolName: 'docs_list_facets-v1_0_0' } },
|
|
178
|
+
}),
|
|
179
|
+
async () => ({ facets: listDocFacets(routes) })
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
return registry;
|
|
183
|
+
}
|
|
@@ -1,184 +1,18 @@
|
|
|
1
|
-
import {
|
|
2
|
-
definePrompt,
|
|
3
|
-
defineResourceTemplate,
|
|
4
|
-
installOp,
|
|
5
|
-
OperationSpecRegistry,
|
|
6
|
-
PromptRegistry,
|
|
7
|
-
ResourceRegistry,
|
|
8
|
-
} from '@contractspec/lib.contracts-spec';
|
|
9
|
-
import type { DocPresentationRoute } from '@contractspec/lib.contracts-spec/docs';
|
|
10
1
|
import { defaultDocRegistry } from '@contractspec/lib.contracts-spec/docs';
|
|
11
|
-
import z from 'zod';
|
|
12
|
-
import { docsSearchSpec } from '../../features/docs';
|
|
13
2
|
import { appLogger } from '../../infrastructure/elysia/logger';
|
|
14
3
|
import { createMcpElysiaHandler } from './common';
|
|
4
|
+
import { buildDocPrompts } from './docsMcp.prompts';
|
|
5
|
+
import { buildDocResources } from './docsMcp.resources';
|
|
6
|
+
import { buildDocOps } from './docsMcp.tools';
|
|
15
7
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
function buildDocResources(routes: DocPresentationRoute[]) {
|
|
20
|
-
const resources = new ResourceRegistry();
|
|
21
|
-
|
|
22
|
-
resources.register(
|
|
23
|
-
defineResourceTemplate({
|
|
24
|
-
meta: {
|
|
25
|
-
uriTemplate: 'docs://list',
|
|
26
|
-
title: 'DocBlocks index',
|
|
27
|
-
description:
|
|
28
|
-
'All registered DocBlocks with route, visibility, tags, and summary.',
|
|
29
|
-
mimeType: 'application/json',
|
|
30
|
-
tags: DOC_TAGS,
|
|
31
|
-
},
|
|
32
|
-
input: z.object({}),
|
|
33
|
-
resolve: async () => {
|
|
34
|
-
const docs = routes.map(({ block, route }) => ({
|
|
35
|
-
id: block.id,
|
|
36
|
-
title: block.title,
|
|
37
|
-
summary: block.summary ?? '',
|
|
38
|
-
tags: block.tags ?? [],
|
|
39
|
-
visibility: block.visibility ?? 'public',
|
|
40
|
-
route,
|
|
41
|
-
}));
|
|
42
|
-
|
|
43
|
-
return {
|
|
44
|
-
uri: 'docs://list',
|
|
45
|
-
mimeType: 'application/json',
|
|
46
|
-
data: JSON.stringify(docs, null, 2),
|
|
47
|
-
};
|
|
48
|
-
},
|
|
49
|
-
})
|
|
50
|
-
);
|
|
51
|
-
|
|
52
|
-
resources.register(
|
|
53
|
-
defineResourceTemplate({
|
|
54
|
-
meta: {
|
|
55
|
-
uriTemplate: 'docs://doc/{id}',
|
|
56
|
-
title: 'DocBlock markdown',
|
|
57
|
-
description: 'Fetch DocBlock body by id as markdown.',
|
|
58
|
-
mimeType: 'text/markdown',
|
|
59
|
-
tags: DOC_TAGS,
|
|
60
|
-
},
|
|
61
|
-
input: z.object({ id: z.string() }),
|
|
62
|
-
resolve: async ({ id }) => {
|
|
63
|
-
const found = defaultDocRegistry.get(id);
|
|
64
|
-
if (!found) {
|
|
65
|
-
return {
|
|
66
|
-
uri: `docs://doc/${encodeURIComponent(id)}`,
|
|
67
|
-
mimeType: 'text/plain',
|
|
68
|
-
data: `DocBlock not found: ${id}`,
|
|
69
|
-
};
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
return {
|
|
73
|
-
uri: `docs://doc/${encodeURIComponent(id)}`,
|
|
74
|
-
mimeType: 'text/markdown',
|
|
75
|
-
data: String(found.block.body ?? ''),
|
|
76
|
-
};
|
|
77
|
-
},
|
|
78
|
-
})
|
|
79
|
-
);
|
|
80
|
-
|
|
81
|
-
return resources;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
function buildDocPrompts() {
|
|
85
|
-
const prompts = new PromptRegistry();
|
|
86
|
-
|
|
87
|
-
prompts.register(
|
|
88
|
-
definePrompt({
|
|
89
|
-
meta: {
|
|
90
|
-
key: 'docs.navigator',
|
|
91
|
-
version: '1.0.0',
|
|
92
|
-
title: 'Find relevant ContractSpec docs',
|
|
93
|
-
description: 'Guide agents to pick the right DocBlock by topic or tag.',
|
|
94
|
-
tags: DOC_TAGS,
|
|
95
|
-
stability: 'beta',
|
|
96
|
-
owners: DOC_OWNERS,
|
|
97
|
-
},
|
|
98
|
-
args: [
|
|
99
|
-
{
|
|
100
|
-
name: 'topic',
|
|
101
|
-
description: 'Goal or subject to search for.',
|
|
102
|
-
required: false,
|
|
103
|
-
schema: z.string().optional(),
|
|
104
|
-
},
|
|
105
|
-
{
|
|
106
|
-
name: 'tag',
|
|
107
|
-
description: 'Optional tag filter.',
|
|
108
|
-
required: false,
|
|
109
|
-
schema: z.string().optional(),
|
|
110
|
-
},
|
|
111
|
-
],
|
|
112
|
-
input: z.object({
|
|
113
|
-
topic: z.string().optional(),
|
|
114
|
-
tag: z.string().optional(),
|
|
115
|
-
}),
|
|
116
|
-
render: async ({ topic, tag }) => {
|
|
117
|
-
const parts = [
|
|
118
|
-
{
|
|
119
|
-
type: 'text' as const,
|
|
120
|
-
text: `Use the docs index to choose DocBlocks. If a specific topic is provided, prefer docs whose id/title/summary match it.${topic ? ` Topic: ${topic}.` : ''}${tag ? ` Tag: ${tag}.` : ''}`,
|
|
121
|
-
},
|
|
122
|
-
{
|
|
123
|
-
type: 'resource' as const,
|
|
124
|
-
uri: 'docs://list',
|
|
125
|
-
title: 'DocBlocks index',
|
|
126
|
-
},
|
|
127
|
-
];
|
|
128
|
-
return parts;
|
|
129
|
-
},
|
|
130
|
-
})
|
|
131
|
-
);
|
|
132
|
-
|
|
133
|
-
return prompts;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
function buildDocOps(routes: DocPresentationRoute[]) {
|
|
137
|
-
const registry = new OperationSpecRegistry();
|
|
138
|
-
|
|
139
|
-
// Use the module-level spec from docs.contracts.ts
|
|
140
|
-
installOp(registry, docsSearchSpec, async (args) => {
|
|
141
|
-
const query = args.query?.toLowerCase().trim();
|
|
142
|
-
const tagsFilter = args.tag?.map((t) => t.toLowerCase().trim()) ?? [];
|
|
143
|
-
const visibility = args.visibility?.toLowerCase().trim();
|
|
144
|
-
|
|
145
|
-
const docs = routes
|
|
146
|
-
.map(({ block, route }) => ({
|
|
147
|
-
id: block.id,
|
|
148
|
-
title: block.title,
|
|
149
|
-
summary: block.summary ?? '',
|
|
150
|
-
tags: block.tags ?? [],
|
|
151
|
-
visibility: (block.visibility ?? 'public').toLowerCase(),
|
|
152
|
-
route,
|
|
153
|
-
}))
|
|
154
|
-
.filter((doc) => {
|
|
155
|
-
const matchesQuery = query
|
|
156
|
-
? doc.id.toLowerCase().includes(query) ||
|
|
157
|
-
doc.title.toLowerCase().includes(query) ||
|
|
158
|
-
doc.summary.toLowerCase().includes(query)
|
|
159
|
-
: true;
|
|
160
|
-
const matchesTags = tagsFilter.length
|
|
161
|
-
? tagsFilter.every((t) =>
|
|
162
|
-
doc.tags.some((tag) => tag.toLowerCase().includes(t))
|
|
163
|
-
)
|
|
164
|
-
: true;
|
|
165
|
-
const matchesVisibility = visibility
|
|
166
|
-
? doc.visibility === visibility
|
|
167
|
-
: true;
|
|
168
|
-
return matchesQuery && matchesTags && matchesVisibility;
|
|
169
|
-
});
|
|
170
|
-
|
|
171
|
-
return {
|
|
172
|
-
docs,
|
|
173
|
-
items: docs,
|
|
174
|
-
total: docs.length,
|
|
175
|
-
};
|
|
176
|
-
});
|
|
177
|
-
|
|
178
|
-
return registry;
|
|
8
|
+
interface DocsMcpHandlerOptions {
|
|
9
|
+
includePresentations?: boolean;
|
|
179
10
|
}
|
|
180
11
|
|
|
181
|
-
export function createDocsMcpHandler(
|
|
12
|
+
export function createDocsMcpHandler(
|
|
13
|
+
path = '/api/mcp/docs',
|
|
14
|
+
options: DocsMcpHandlerOptions = {}
|
|
15
|
+
) {
|
|
182
16
|
const routes = defaultDocRegistry.list();
|
|
183
17
|
|
|
184
18
|
return createMcpElysiaHandler({
|
|
@@ -187,7 +21,9 @@ export function createDocsMcpHandler(path = '/api/mcp/docs') {
|
|
|
187
21
|
serverName: 'contractspec-docs-mcp',
|
|
188
22
|
ops: buildDocOps(routes),
|
|
189
23
|
resources: buildDocResources(routes),
|
|
190
|
-
prompts: buildDocPrompts(),
|
|
191
|
-
presentations:
|
|
24
|
+
prompts: buildDocPrompts(routes),
|
|
25
|
+
presentations: options.includePresentations
|
|
26
|
+
? routes.map(({ descriptor }) => descriptor)
|
|
27
|
+
: undefined,
|
|
192
28
|
});
|
|
193
29
|
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
const REQUIRED_ACCEPT_TYPES = ['application/json', 'text/event-stream'];
|
|
2
|
+
|
|
3
|
+
function canNormalizeAcceptHeader(acceptHeader: string | null) {
|
|
4
|
+
return (
|
|
5
|
+
!acceptHeader ||
|
|
6
|
+
acceptHeader.includes('*/*') ||
|
|
7
|
+
acceptHeader.includes('application/*') ||
|
|
8
|
+
REQUIRED_ACCEPT_TYPES.some((value) => acceptHeader.includes(value))
|
|
9
|
+
);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function normalizeMcpRequest(request: Request) {
|
|
13
|
+
if (request.method !== 'POST') return request;
|
|
14
|
+
|
|
15
|
+
const acceptHeader = request.headers.get('accept');
|
|
16
|
+
if (!canNormalizeAcceptHeader(acceptHeader)) return request;
|
|
17
|
+
|
|
18
|
+
const missingTypes = REQUIRED_ACCEPT_TYPES.filter(
|
|
19
|
+
(value) => !acceptHeader?.includes(value)
|
|
20
|
+
);
|
|
21
|
+
if (missingTypes.length === 0) return request;
|
|
22
|
+
|
|
23
|
+
const headers = new Headers(request.headers);
|
|
24
|
+
headers.set(
|
|
25
|
+
'accept',
|
|
26
|
+
[acceptHeader, ...missingTypes].filter(Boolean).join(', ')
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
return new Request(request, { headers });
|
|
30
|
+
}
|