Skip to content

Commit afd9819

Browse files
committed
Fix typo in dhi tool description (#54)
Fix multiple repository tools (#55) * fix some repo tools * fix lint and format add structured content when error (#57)
1 parent 35fb3f3 commit afd9819

File tree

3 files changed

+145
-30
lines changed

3 files changed

+145
-30
lines changed

src/repos.ts

Lines changed: 109 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import { Asset, AssetConfig } from './asset';
1919
import { z } from 'zod';
2020
import { createPaginatedResponseSchema } from './types';
2121
import { CallToolResult } from '@modelcontextprotocol/sdk/types';
22+
import { logger } from './logger';
2223

2324
//#region Types
2425
// all items in the types are optional and nullable because structured content is always evaluated even when an error occurs.
@@ -126,8 +127,9 @@ const CreateRepositoryRequest = z.object({
126127
namespace: z.string().describe('The namespace of the repository. Required.'),
127128
name: z
128129
.string()
130+
.default('')
129131
.describe(
130-
'The name of the repository. Must contain a combination of alphanumeric characters and may contain the special characters ., _, or -. Letters must be lowercase. Required.'
132+
'The name of the repository (required). Must contain a combination of alphanumeric characters and may contain the special characters ., _, or -. Letters must be lowercase.'
131133
),
132134
description: z.string().optional().describe('The description of the repository'),
133135
is_private: z.boolean().optional().describe('Whether the repository is private'),
@@ -288,7 +290,7 @@ export class Repos extends Asset {
288290
'createRepository',
289291
{
290292
description:
291-
'Create a new repository in the given namespace. User must pass the repository name and if the repository has to be public or private. Can optionally pass a description.',
293+
'Create a new repository in the given namespace. You MUST ask the user for the repository name and if the repository has to be public or private. Can optionally pass a description.\nIMPORTANT: Before calling this tool, you must ensure you have:\n - The repository name (name).',
292294
inputSchema: CreateRepositoryRequest.shape,
293295
outputSchema: Repository.shape,
294296
annotations: {
@@ -305,7 +307,14 @@ export class Repos extends Asset {
305307
'getRepositoryInfo',
306308
{
307309
description: 'Get the details of a repository in the given namespace.',
308-
inputSchema: z.object({ namespace: z.string(), repository: z.string() }).shape,
310+
inputSchema: z.object({
311+
namespace: z
312+
.string()
313+
.describe(
314+
'The namespace of the repository (required). If not provided the `library` namespace will be used for official images.'
315+
),
316+
repository: z.string().describe('The repository name (required)'),
317+
}).shape,
309318
outputSchema: Repository.shape,
310319
annotations: {
311320
title: 'Get Repository Info',
@@ -321,13 +330,33 @@ export class Repos extends Asset {
321330
this.server.registerTool(
322331
'updateRepositoryInfo',
323332
{
324-
description: 'Update the details of a repository in the given namespace.',
333+
description:
334+
'Update the details of a repository in the given namespace. Description, overview and status are the only fields that can be updated. While description and overview changes are fine, a status change is a dangerous operation so the user must explicitly ask for it.',
325335
inputSchema: z.object({
326-
namespace: z.string(),
327-
repository: z.string(),
328-
description: z.string().optional(),
329-
full_description: z.string().max(25000).optional(),
330-
status: z.number().optional(),
336+
namespace: z
337+
.string()
338+
.describe('The namespace of the repository (required)'),
339+
repository: z.string().describe('The repository name (required)'),
340+
description: z
341+
.string()
342+
.optional()
343+
.describe(
344+
'The description of the repository. If user asks for updating the description of the repository, this is the field that should be updated.'
345+
),
346+
full_description: z
347+
.string()
348+
.max(25000)
349+
.optional()
350+
.describe(
351+
'The full description (overview)of the repository. If user asks for updating the full description or the overview of the repository, this is the field that should be updated. '
352+
),
353+
status: z
354+
.enum(['active', 'inactive'])
355+
.optional()
356+
.nullable()
357+
.describe(
358+
'The status of the repository. If user asks for updating the status of the repository, this is the field that should be updated. This is a dangerous operation and should be done with caution so user must be prompted to confirm the operation. Valid status are `active` (1) and `inactive` (0). Normally do not update the status if it is not strictly required by the user. It is not possible to change an `inactive` repository to `active` if it has no images.'
359+
),
331360
}).shape,
332361
outputSchema: Repository.shape,
333362
annotations: {
@@ -530,6 +559,7 @@ export class Repos extends Asset {
530559
): Promise<CallToolResult> {
531560
// sometimes the mcp client tries to pass a default repository name. Fail in this case.
532561
if (!request.name || request.name === 'new-repository') {
562+
logger.error('Repository name is required.');
533563
throw new Error('Repository name is required.');
534564
}
535565
const url = `${this.config.host}/namespaces/${request.namespace}/repositories`;
@@ -549,16 +579,25 @@ export class Repos extends Asset {
549579
repository: string;
550580
}): Promise<CallToolResult> {
551581
if (!namespace || !repository) {
582+
logger.error('Namespace and repository name are required');
552583
throw new Error('Namespace and repository name are required');
553584
}
585+
logger.info(`Getting info for repository ${repository} in ${namespace}`);
554586
const url = `${this.config.host}/namespaces/${namespace}/repositories/${repository}`;
555587

556-
return this.callAPI<z.infer<typeof Repository>>(
588+
const response = await this.callAPI<z.infer<typeof Repository>>(
557589
url,
558590
{ method: 'GET' },
559591
`Here are the details of the repository :${repository} in ${namespace}. :response`,
560592
`Error getting repository info for ${repository} in ${namespace}`
561593
);
594+
if (namespace === 'library') {
595+
response.content.push({
596+
type: 'text',
597+
text: `This is an official image from Docker Hub. You can access it at https://hub.docker.com/_/${repository}.\nIf you did not ask for an official image, please call this tool again and clearly specify a namespace.`,
598+
});
599+
}
600+
return response;
562601
}
563602

564603
private async updateRepositoryInfo({
@@ -572,23 +611,76 @@ export class Repos extends Asset {
572611
repository: string;
573612
description?: string;
574613
full_description?: string;
575-
status?: number;
614+
status?: string | null;
576615
}): Promise<CallToolResult> {
616+
const extraContent: { type: 'text'; text: string }[] = [];
577617
if (!namespace || !repository) {
578618
throw new Error('Namespace and repository name are required');
579619
}
620+
logger.info(
621+
`Updating repository ${repository} in ${namespace} with description: ${description}, full_description: ${full_description}, status: ${status}`
622+
);
580623
const url = `${this.config.host}/namespaces/${namespace}/repositories/${repository}`;
581-
const body = {
582-
description,
583-
full_description,
584-
status,
585-
};
586-
return this.callAPI<z.infer<typeof Repository>>(
624+
const body: { description?: string; full_description?: string; status?: number } = {};
625+
if (description && description !== '') {
626+
body.description = description;
627+
}
628+
if (full_description && full_description !== '') {
629+
body.full_description = full_description;
630+
}
631+
if (status !== undefined) {
632+
// get current repository info to check if a status change is needed
633+
const currentRepository = await this.getRepositoryInfo({ namespace, repository });
634+
if (currentRepository.isError) {
635+
logger.error(`Error getting repository info for ${repository} in ${namespace}`);
636+
return {
637+
isError: true,
638+
content: [
639+
{
640+
type: 'text',
641+
text: `Error getting repository info for ${repository} in ${namespace}`,
642+
},
643+
],
644+
};
645+
}
646+
const currentStatus = (
647+
currentRepository.structuredContent as z.infer<typeof Repository>
648+
).status;
649+
if (currentStatus !== status) {
650+
logger.info(
651+
`Repository ${repository} in ${namespace} is currently in status ${currentStatus}. Updating to ${status}.`
652+
);
653+
if (status === 'active') {
654+
return {
655+
isError: true,
656+
content: [
657+
{
658+
type: 'text',
659+
text: `Repository ${repository} in ${namespace} is currently inactive. It is not possible to change an inactive repository to active if it has no images. If you did not ask for updating the status of the repository, please call this tool again and specifically ask for updating only the description or the overview of the repository.`,
660+
},
661+
],
662+
structuredContent: {
663+
error: `Repository ${repository} in ${namespace} is currently inactive. It is not possible to change an inactive repository to active if it has no images. If you did not ask for updating the status of the repository, please call this tool again and specifically ask for updating only the description or the overview of the repository.`,
664+
},
665+
};
666+
}
667+
body.status = status === 'active' ? 1 : 0;
668+
extraContent.push({
669+
type: 'text',
670+
text: `Requested a status change from ${currentStatus} to ${status}. This is potentially a dangerous operation and should be done with caution. If you are not sure, please go on Docker Hub and revert the status manually.\nhttps://hub.docker.com/r/${namespace}/${repository}`,
671+
});
672+
}
673+
}
674+
const response = await this.callAPI<z.infer<typeof Repository>>(
587675
url,
588676
{ method: 'PATCH', body: JSON.stringify(body) },
589677
`Repository ${repository} updated successfully. :response`,
590678
`Error updating repository ${repository}`
591679
);
680+
if (extraContent.length > 0) {
681+
response.content = [...response.content, ...extraContent];
682+
}
683+
return response;
592684
}
593685

594686
private async getRepositoryTag({

src/scout.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ export class ScoutAPI extends Asset {
6262
'docker-hardened-images',
6363
{
6464
description:
65-
'This API is used to list Docker Hardened Images (DHIs) mirrored into one of the organisations of the user from the dhi organisation. Must be always prompted to input the organisation by the user. Docker Hardened Images are the most secure, minimal, production-ready images available, with near-zero CVEs and enterprise-grade SLA. Should be used to search for secure image in an organisation.',
65+
'This API is used to list Docker Hardened Images (DHIs) mirrored into one of the organisations of the user from the dhi organisation. Must be always prompted to input the organisation by the user. Docker Hardened Images are the most secure, minimal, production-ready images available, with near-zero CVEs and enterprise-grade SLA. Should be used to search for secure images in an organisation.',
6666
inputSchema: z.object({
6767
organisation: z
6868
.string()

tools.json

Lines changed: 35 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -618,7 +618,7 @@
618618
},
619619
{
620620
"name": "createRepository",
621-
"description": "Create a new repository in the given namespace. User must pass the repository name and if the repository has to be public or private. Can optionally pass a description.",
621+
"description": "Create a new repository in the given namespace. You MUST ask the user for the repository name and if the repository has to be public or private. Can optionally pass a description.\nIMPORTANT: Before calling this tool, you must ensure you have:\n - The repository name (name).",
622622
"inputSchema": {
623623
"type": "object",
624624
"properties": {
@@ -628,7 +628,8 @@
628628
},
629629
"name": {
630630
"type": "string",
631-
"description": "The name of the repository. Must contain a combination of alphanumeric characters and may contain the special characters ., _, or -. Letters must be lowercase. Required."
631+
"default": "",
632+
"description": "The name of the repository (required). Must contain a combination of alphanumeric characters and may contain the special characters ., _, or -. Letters must be lowercase."
632633
},
633634
"description": {
634635
"type": "string",
@@ -648,7 +649,7 @@
648649
"description": "The registry to create the repository in"
649650
}
650651
},
651-
"required": ["namespace", "name"],
652+
"required": ["namespace"],
652653
"additionalProperties": false,
653654
"$schema": "http://json-schema.org/draft-07/schema#"
654655
},
@@ -1154,10 +1155,12 @@
11541155
"type": "object",
11551156
"properties": {
11561157
"namespace": {
1157-
"type": "string"
1158+
"type": "string",
1159+
"description": "The namespace of the repository (required). If not provided the `library` namespace will be used for official images."
11581160
},
11591161
"repository": {
1160-
"type": "string"
1162+
"type": "string",
1163+
"description": "The repository name (required)"
11611164
}
11621165
},
11631166
"required": ["namespace", "repository"],
@@ -1661,25 +1664,45 @@
16611664
},
16621665
{
16631666
"name": "updateRepositoryInfo",
1664-
"description": "Update the details of a repository in the given namespace.",
1667+
"description": "Update the details of a repository in the given namespace. Description, overview and status are the only fields that can be updated. While description and overview changes are fine, a status change is a dangerous operation so the user must explicitly ask for it.",
16651668
"inputSchema": {
16661669
"type": "object",
16671670
"properties": {
16681671
"namespace": {
1669-
"type": "string"
1672+
"type": "string",
1673+
"description": "The namespace of the repository (required)"
16701674
},
16711675
"repository": {
1672-
"type": "string"
1676+
"type": "string",
1677+
"description": "The repository name (required)"
16731678
},
16741679
"description": {
1675-
"type": "string"
1680+
"type": "string",
1681+
"description": "The description of the repository. If user asks for updating the description of the repository, this is the field that should be updated."
16761682
},
16771683
"full_description": {
16781684
"type": "string",
1679-
"maxLength": 25000
1685+
"maxLength": 25000,
1686+
"description": "The full description (overview)of the repository. If user asks for updating the full description or the overview of the repository, this is the field that should be updated. "
16801687
},
16811688
"status": {
1682-
"type": "number"
1689+
"anyOf": [
1690+
{
1691+
"anyOf": [
1692+
{
1693+
"not": {}
1694+
},
1695+
{
1696+
"type": "string",
1697+
"enum": ["active", "inactive"]
1698+
}
1699+
]
1700+
},
1701+
{
1702+
"type": "null"
1703+
}
1704+
],
1705+
"description": "The status of the repository. If user asks for updating the status of the repository, this is the field that should be updated. This is a dangerous operation and should be done with caution so user must be prompted to confirm the operation. Valid status are `active` (1) and `inactive` (0). Normally do not update the status if it is not strictly required by the user. It is not possible to change an `inactive` repository to `active` if it has no images."
16831706
}
16841707
},
16851708
"required": ["namespace", "repository"],
@@ -3756,7 +3779,7 @@
37563779
},
37573780
{
37583781
"name": "docker-hardened-images",
3759-
"description": "This API is used to list Docker Hardened Images (DHIs) mirrored into one of the organisations of the user from the dhi organisation. Must be always prompted to input the organisation by the user. Docker Hardened Images are the most secure, minimal, production-ready images available, with near-zero CVEs and enterprise-grade SLA. Should be used to search for secure image in an organisation.",
3782+
"description": "This API is used to list Docker Hardened Images (DHIs) mirrored into one of the organisations of the user from the dhi organisation. Must be always prompted to input the organisation by the user. Docker Hardened Images are the most secure, minimal, production-ready images available, with near-zero CVEs and enterprise-grade SLA. Should be used to search for secure images in an organisation.",
37603783
"inputSchema": {
37613784
"type": "object",
37623785
"properties": {

0 commit comments

Comments
 (0)