Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
159 changes: 159 additions & 0 deletions MCP/McpServer/Services/McpSessionManager.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System.Collections.Concurrent;
using System.Text;
using System.Text.Json;
using System.Text.Json.Nodes;
using McpServer.Data;
using Microsoft.AspNetCore.Http.Features;
Expand Down Expand Up @@ -317,6 +318,29 @@ private static string BuildToolListResponse(JsonNode id) =>
["properties"] = new JsonObject(),
["required"] = new JsonArray()
}
},
new JsonObject
{
["name"] = "generate_questionnaire",
["description"] = "Generates a new questionnaire with meaningful example content based on the structure of an existing questionnaire. Each entry is pre-filled with a sensible default answer drawn from the available examples. Returns a Markdown summary and an importable JSON object for the generated questionnaire.",
["inputSchema"] = new JsonObject
{
["type"] = "object",
["properties"] = new JsonObject
{
["questionnaireId"] = new JsonObject
{
["type"] = "string",
["description"] = "The ID (or name) of the questionnaire to use as a structural template."
},
["name"] = new JsonObject
{
["type"] = "string",
["description"] = "Optional. The name for the generated questionnaire. Defaults to '<source name> (Generated)'."
}
},
["required"] = new JsonArray { "questionnaireId" }
}
}
}
});
Expand All @@ -335,6 +359,7 @@ private async Task<string> BuildToolCallResponseAsync(JsonNode id, JsonNode? @pa
"list_questionnaires" => BuildListQuestionnairesResponse(id, excludedIds, referenceId),
"get_answers_for_category" => BuildAnswersForCategoryResponse(id, args, excludedIds),
"get_tech_radar" => BuildTechRadarResponse(id),
"generate_questionnaire" => BuildGenerateQuestionnaireResponse(id, args),
_ => BuildError(id, -32602, $"Unknown tool: {toolName}")
};
}
Expand Down Expand Up @@ -508,6 +533,140 @@ private string BuildTechRadarResponse(JsonNode id)
return BuildTextToolResponse(id, sb.ToString());
}

private string BuildGenerateQuestionnaireResponse(JsonNode id, JsonNode? args)
{
var questionnaireId = args?["questionnaireId"]?.GetValue<string>();
var name = args?["name"]?.GetValue<string>();

if (string.IsNullOrWhiteSpace(questionnaireId))
return BuildError(id, -32602, "Parameter 'questionnaireId' is required.");

var workspace = _repo.Current;
if (workspace is null)
return BuildTextToolResponse(id, NotLoadedMessage);

var source = workspace.Questionnaires.FirstOrDefault(q =>
q.Id.Equals(questionnaireId, StringComparison.OrdinalIgnoreCase) ||
q.Name.Equals(questionnaireId, StringComparison.OrdinalIgnoreCase));

if (source is null)
return BuildError(id, -32602,
$"Questionnaire '{questionnaireId}' not found in the loaded workspace.");

var generatedName = string.IsNullOrWhiteSpace(name)
? $"{source.Name} (Generated)"
: name;

var generatedId = Guid.NewGuid().ToString("N")[..8];

// Build a JSON representation of the generated questionnaire.
var categories = new JsonArray();

foreach (var cat in source.Categories)
{
if (cat.IsMetadata == true)
{
// Preserve the metadata category structure but reset personal data.
var metaObj = new JsonObject
{
["id"] = cat.Id,
["title"] = cat.Title,
["desc"] = cat.Desc,
["isMetadata"] = true,
["metadata"] = new JsonObject
{
["productName"] = generatedName,
["company"] = "",
["department"] = "",
["contactPerson"] = "",
["description"] = $"Generated from '{source.Name}'",
["executionType"] = cat.Metadata?.ExecutionType ?? "Not specified",
["architecturalRole"] = cat.Metadata?.ArchitecturalRole ?? "Not specified"
}
};
categories.Add(metaObj);
continue;
}

// For regular categories, pre-fill each entry with the first available example.
var entries = new JsonArray();

foreach (var entry in cat.Entries ?? [])
{
var firstExample = entry.Examples?.FirstOrDefault();
var answers = new JsonArray();

if (firstExample is not null)
{
answers.Add(new JsonObject
{
["technology"] = firstExample.Label,
["status"] = "adopt",
["comments"] = string.IsNullOrWhiteSpace(firstExample.Description)
? null
: (JsonNode)firstExample.Description
});
}

entries.Add(new JsonObject
{
["id"] = entry.Id,
["aspect"] = entry.Aspect,
["answers"] = answers
});
}

categories.Add(new JsonObject
{
["id"] = cat.Id,
["title"] = cat.Title,
["desc"] = cat.Desc,
["entries"] = entries
});
}

var generated = new JsonObject
{
["id"] = generatedId,
["name"] = generatedName,
["categories"] = categories
};

// Build a human-readable summary together with the importable JSON.
var sb = new StringBuilder();
sb.AppendLine($"# Generated Questionnaire: {generatedName}");
sb.AppendLine();
sb.AppendLine($"Based on template: **{source.Name}** (`{source.Id}`)");
sb.AppendLine($"Generated ID: `{generatedId}`");
sb.AppendLine();
sb.AppendLine("## Pre-filled entries");
sb.AppendLine();

foreach (var cat in source.Categories.Where(c => c.IsMetadata != true))
{
sb.AppendLine($"### {cat.Title}");
foreach (var entry in cat.Entries ?? [])
{
var firstExample = entry.Examples?.FirstOrDefault();
if (firstExample is not null)
sb.AppendLine($" - **{entry.Aspect}**: {firstExample.Label} *(adopt)*");
else
sb.AppendLine($" - **{entry.Aspect}**: *(no examples available)*");
}
sb.AppendLine();
}

sb.AppendLine("## Importable JSON");
sb.AppendLine();
sb.AppendLine("Copy the JSON below and import it into SolutionInventory as a new questionnaire:");
sb.AppendLine();
sb.AppendLine("```json");
sb.AppendLine(generated.ToJsonString(new JsonSerializerOptions { WriteIndented = true }));
sb.AppendLine("```");

return BuildTextToolResponse(id, sb.ToString());
}

private const string NotLoadedMessage =
"No workspace loaded. Use the management UI (Workspace tab → Load Example or Load File) to load a workspace first.";

Expand Down
18 changes: 18 additions & 0 deletions mcp-bridge.js
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,24 @@ async function handleListTools(params, id) {
properties: {},
required: []
}
},
{
name: 'generate_questionnaire',
description: 'Generates a new questionnaire with meaningful example content based on the structure of an existing questionnaire. Each entry is pre-filled with a sensible default answer drawn from the available examples. Returns a Markdown summary and an importable JSON object for the generated questionnaire.',
inputSchema: {
type: 'object',
properties: {
questionnaireId: {
type: 'string',
description: 'The ID (or name) of the questionnaire to use as a structural template'
},
name: {
type: 'string',
description: "Optional. The name for the generated questionnaire. Defaults to '<source name> (Generated)'"
}
},
required: ['questionnaireId']
}
}
];

Expand Down