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
24 changes: 0 additions & 24 deletions .azure-pipelines/common/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,30 +7,6 @@ steps:
displayName: 'Start X Virtual Frame Buffer'
condition: eq(variables['Agent.OS'], 'Linux')

# Tempoary workaround for https://github.com/ballerina-platform/ballerina-distribution/issues/4537
- script: |
curl -o ballerina.deb 'https://dist.ballerina.io/downloads/2201.6.0/ballerina-2201.6.0-swan-lake-linux-x64.deb'
sudo dpkg -i ballerina.deb
rm -f ballerina.deb
displayName: Install Ballerina(Linux)
condition: eq(variables['Agent.OS'], 'Linux')

- script: |
curl -o ballerina.pkg 'https://dist.ballerina.io/downloads/2201.6.0/ballerina-2201.6.0-swan-lake-macos-x64.pkg'
sudo installer -pkg ballerina.pkg -target /
rm -f ballerina.pkg
echo '##vso[task.prependpath]/Library/Ballerina/bin'
displayName: Install Ballerina(MacOS)
condition: eq(variables['Agent.OS'], 'Darwin')

- script: |
curl -o ballerina.msi https://dist.ballerina.io/downloads/2201.6.0/ballerina-2201.6.0-swan-lake-windows-x64.msi
msiexec /i ballerina.msi /quiet /qr /L*V "C:\Temp\msilog.log"
del ballerina.msi
echo "##vso[task.setvariable variable=PATH]C:\Program Files\Ballerina\bin;$(PATH)"
displayName: Install Ballerina(Windows)
condition: eq(variables['Agent.OS'], 'Windows_NT')

- task: UsePythonVersion@0
displayName: 'Use Python 3.7.x'
inputs:
Expand Down
6 changes: 5 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -1118,7 +1118,6 @@
"PowerShell",
"Python",
"TypeScript",
"Ballerina",
"Custom"
],
"description": "%azureFunctions.projectLanguage%",
Expand Down Expand Up @@ -1365,6 +1364,11 @@
"type": "boolean",
"description": "%azureFunctions.allowProgrammingModelSelection%",
"default": false
},
"azureFunctions.showBallerinaProjectCreation": {
"type": "boolean",
"description": "%azureFunctions.showBallerinaProjectCreation%",
"default": false
}
}
}
Expand Down
3 changes: 2 additions & 1 deletion package.nls.json
Original file line number Diff line number Diff line change
Expand Up @@ -138,5 +138,6 @@
"azureFunctions.durableTaskScheduler.enablePreviewFeatures": "Enable Durable Task Scheduler preview features",
"azureFunctions.durableTaskScheduler.emulatorRegistry": "The registry of the Durable Task Scheduler emulator image.",
"azureFunctions.durableTaskScheduler.emulatorImage": "The name of the Durable Task Scheduler emulator image.",
"azureFunctions.durableTaskScheduler.emulatorTag": "The tag of the Durable Task Scheduler emulator image."
"azureFunctions.durableTaskScheduler.emulatorTag": "The tag of the Durable Task Scheduler emulator image.",
"azureFunctions.showBallerinaProjectCreation": "Show the option to create a Ballerina Function projects when creating a new Function project."
}
4 changes: 3 additions & 1 deletion src/commands/createFunction/FunctionListStep.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,9 @@ export class FunctionListStep extends AzureWizardPromptStep<IFunctionWizardConte
}

public shouldPrompt(context: IFunctionWizardContext): boolean {
return !context.functionTemplate && context['buildTool'] !== JavaBuildTool.maven;
return !context.functionTemplate &&
context['buildTool'] !== JavaBuildTool.maven &&
context.language !== ProjectLanguage.SelfHostedMCPServer;
}

private async getPicks(context: IFunctionWizardContext, templateFilter: TemplateFilter): Promise<IAzureQuickPickItem<FunctionTemplateBase | TemplatePromptResult>[]> {
Expand Down
5 changes: 5 additions & 0 deletions src/commands/createNewProject/IProjectWizardContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,9 @@ export interface IProjectWizardContext extends IActionContext {
containerizedProject?: boolean;
}

export interface MCPProjectWizardContext extends IProjectWizardContext {
serverLanguage?: ProjectLanguage;
includeSampleCode?: boolean;
}

export type OpenBehavior = 'AddToWorkspace' | 'OpenInNewWindow' | 'OpenInCurrentWindow' | 'AlreadyOpen' | 'DontOpen';
19 changes: 16 additions & 3 deletions src/commands/createNewProject/NewProjectLanguageStep.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@

import { AzureWizardPromptStep, type AzureWizardExecuteStep, type IAzureQuickPickItem, type IWizardOptions } from '@microsoft/vscode-azext-utils';
import { type QuickPickOptions } from 'vscode';
import { ProjectLanguage, nodeDefaultModelVersion, nodeLearnMoreLink, nodeModels, pythonDefaultModelVersion, pythonLearnMoreLink, pythonModels } from '../../constants';
import { ProjectLanguage, nodeDefaultModelVersion, nodeLearnMoreLink, nodeModels, pythonDefaultModelVersion, pythonLearnMoreLink, pythonModels, showBallerinaProjectCreationSetting } from '../../constants';
import { localize } from '../../localize';
import { TemplateSchemaVersion } from '../../templates/TemplateProviderBase';
import { nonNullProp } from '../../utils/nonNull';
import { getWorkspaceSetting } from '../../vsCodeConfig/settings';
import { FunctionListStep } from '../createFunction/FunctionListStep';
import { addInitVSCodeSteps } from '../initProjectForVSCode/InitVSCodeLanguageStep';
import { type IProjectWizardContext } from './IProjectWizardContext';
Expand All @@ -23,6 +24,10 @@ import { TypeScriptProjectCreateStep } from './ProjectCreateStep/TypeScriptProje
import { addBallerinaCreateProjectSteps } from './ballerinaSteps/addBallerinaCreateProjectSteps';
import { DotnetRuntimeStep } from './dotnetSteps/DotnetRuntimeStep';
import { addJavaCreateProjectSteps } from './javaSteps/addJavaCreateProjectSteps';
import { MCPDownloadSampleCodeExecuteStep } from './mcpServerSteps/MCPDownloadSampleCodeExecuteStep';
import { MCPIncludeSampleCodePromptStep } from './mcpServerSteps/MCPIncludeSampleCodePromptStep';
import { MCPProjectCreateStep } from './mcpServerSteps/MCPProjectCreateStep';
import { MCPServerLanguagePromptStep } from './mcpServerSteps/MCPServerLanguagePromptStep';

export class NewProjectLanguageStep extends AzureWizardPromptStep<IProjectWizardContext> {
public hideStepCount: boolean = true;
Expand All @@ -44,11 +49,15 @@ export class NewProjectLanguageStep extends AzureWizardPromptStep<IProjectWizard
{ label: ProjectLanguage.CSharp, data: { language: ProjectLanguage.CSharp } },
{ label: ProjectLanguage.Python, data: { language: ProjectLanguage.Python } },
{ label: ProjectLanguage.Java, data: { language: ProjectLanguage.Java } },
{ label: ProjectLanguage.Ballerina, data: { language: ProjectLanguage.Ballerina } },
{ label: ProjectLanguage.PowerShell, data: { language: ProjectLanguage.PowerShell } },
{ label: localize('customHandler', 'Custom Handler'), data: { language: ProjectLanguage.Custom } }
{ label: localize('customHandler', 'Custom Handler'), data: { language: ProjectLanguage.Custom } },
{ label: localize('selfHostedMCPServer', 'Self-hosted MCP server'), data: { language: ProjectLanguage.SelfHostedMCPServer } }
];

if (getWorkspaceSetting(showBallerinaProjectCreationSetting)) {
languagePicks.push({ label: ProjectLanguage.Ballerina, data: { language: ProjectLanguage.Ballerina } });
}

if (context.languageFilter) {
languagePicks = languagePicks.filter(p => {
return context.languageFilter?.test(p.data.language);
Expand Down Expand Up @@ -116,6 +125,10 @@ export class NewProjectLanguageStep extends AzureWizardPromptStep<IProjectWizard
case ProjectLanguage.Custom:
executeSteps.push(new CustomProjectCreateStep());
break;
case ProjectLanguage.SelfHostedMCPServer:
promptSteps.push(new MCPServerLanguagePromptStep(), new MCPIncludeSampleCodePromptStep());
executeSteps.push(new MCPDownloadSampleCodeExecuteStep(), new MCPProjectCreateStep());
break;
default:
executeSteps.push(new ScriptProjectCreateStep());
break;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { AzExtFsExtra, AzureWizardExecuteStepWithActivityOutput } from '@microsoft/vscode-azext-utils';
import * as path from 'path';
import { type Progress } from 'vscode';
import { type GitHubFileMetadata } from '../../../constants';
import { localize } from "../../../localize";
import { feedUtils } from '../../../utils/feedUtils';
import { nonNullProp } from '../../../utils/nonNull';
import { parseJson } from '../../../utils/parseJson';
import { requestUtils } from '../../../utils/requestUtils';
import { type MCPProjectWizardContext } from '../IProjectWizardContext';

export class MCPDownloadSampleCodeExecuteStep extends AzureWizardExecuteStepWithActivityOutput<MCPProjectWizardContext> {
stepName: string = 'MCPDownloadSampleCodeExecuteStep';
protected getTreeItemLabel(context: MCPProjectWizardContext): string {
return localize('downloadSampleCode', 'Downloading {0} sample server code', nonNullProp(context, 'serverLanguage'));
}
protected getOutputLogSuccess(context: MCPProjectWizardContext): string {
return localize('downloadSampleCodeSuccess', 'Successfully downloaded {0} sample server code"', nonNullProp(context, 'serverLanguage'));
}
protected getOutputLogFail(context: MCPProjectWizardContext): string {
return localize('downloadSampleCodeFail', 'Failed to download {0} sample server code', nonNullProp(context, 'serverLanguage'));
}
protected getOutputLogProgress(context: MCPProjectWizardContext): string {
return localize('downloadingSampleCode', 'Downloading {0} sample server code...', nonNullProp(context, 'serverLanguage'));
}

public priority: number = 12;

public async execute(context: MCPProjectWizardContext, progress: Progress<{ message?: string | undefined; increment?: number | undefined }>): Promise<void> {
progress.report({ message: localize('downloadingSampleCodeExecute', 'Downloading sample code...') });
// TODO: change this to aka.ms link when samples repo is available, this is a placeholder
const sampleFilesUrl =
'https://api.github.com/repos/Azure-Samples/mcp-sdk-functions-hosting-python/contents';
const sampleFiles: GitHubFileMetadata[] = await feedUtils.getJsonFeed(context, sampleFilesUrl);
await this.downloadFilesRecursively(context, sampleFiles, context.projectPath);
}

private async downloadFilesRecursively(context: MCPProjectWizardContext, items: GitHubFileMetadata[], basePath: string): Promise<void> {
for (const item of items) {
if (item.type === 'file') {
// Download file content
// not a JSON so this is throwing errors:
const response = await requestUtils.sendRequestWithExtTimeout(context, { method: 'GET', url: item.download_url });
const fileContent = response.bodyAsText;
const filePath: string = path.join(basePath, item.name);
await AzExtFsExtra.writeFile(filePath, fileContent ?? '');
} else if (item.type === 'dir') {
// Create directory
const dirPath: string = path.join(basePath, item.name);
await AzExtFsExtra.ensureDir(dirPath);

// Get directory contents
const response = await requestUtils.sendRequestWithExtTimeout(context, { method: 'GET', url: item.url });
const dirContents: GitHubFileMetadata[] = parseJson<GitHubFileMetadata[]>(nonNullProp(response, 'bodyAsText'));

// Recursively download directory contents
await this.downloadFilesRecursively(context, dirContents, dirPath);
}
}
}

public shouldExecute(context: MCPProjectWizardContext): boolean {
return !!context.includeSampleCode;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { AzureWizardPromptStep, type IAzureQuickPickItem } from "@microsoft/vscode-azext-utils";
import { type QuickPickOptions } from "vscode";
import { localize } from "../../../localize";
import { type MCPProjectWizardContext } from "../IProjectWizardContext";

export class MCPIncludeSampleCodePromptStep extends AzureWizardPromptStep<MCPProjectWizardContext> {
public hideStepCount: boolean = true;

public constructor() {
super();
}

public shouldPrompt(wizardContext: MCPProjectWizardContext): boolean {
return !wizardContext.includeSampleCode;
}

public async prompt(context: MCPProjectWizardContext): Promise<void> {
const options: QuickPickOptions = { placeHolder: localize('includeSampleCode', 'Include sample server code') };
const result = (await context.ui.showQuickPick(this.getPicks(), options)).data;
context.includeSampleCode = result.includeSampleCode;
}

public getPicks(): IAzureQuickPickItem<{ includeSampleCode: boolean }>[] {
return [
{ label: localize('yes', 'Yes'), data: { includeSampleCode: true } },
{ label: localize('no', 'No'), data: { includeSampleCode: false } }
];
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { type Progress } from "vscode";
import { azureWebJobsFeatureFlags, enableMcpCustomHandlerPreview, ProjectLanguage, workerRuntimeKey } from "../../../constants";
import { type IHostJsonV2 } from "../../../funcConfig/host";
import { type MCPProjectWizardContext } from "../IProjectWizardContext";
import { ScriptProjectCreateStep } from "../ProjectCreateStep/ScriptProjectCreateStep";
export class MCPProjectCreateStep extends ScriptProjectCreateStep {
public async executeCore(context: MCPProjectWizardContext, _progress: Progress<{ message?: string | undefined; increment?: number | undefined; }>): Promise<void> {
this.localSettingsJson.Values = this.localSettingsJson.Values || {};
this.localSettingsJson.Values[azureWebJobsFeatureFlags] = enableMcpCustomHandlerPreview;
// TODO: need to fix this to be plain strings: "python", "node", "dotnet-isolated"
this.localSettingsJson.Values[workerRuntimeKey] = context.serverLanguage ?? 'custom'
// TODO: We may need to create the mcp.json like this, unclear atm
/* {
"servers": {
"local-mcp-server": {
"type": "http",
"url": "http://localhost:7071/mcp"
},
"remote-mcp-server": {
"type": "http",
"url": "https://${input:functionapp-domain}/mcp",
}
},
"inputs": [
{
"type": "promptString",
"id": "functionapp-domain",
"description": "The domain of the function app."
}
]
} */
await super.executeCore(context, _progress);
return;
}

protected async getHostContent(context: MCPProjectWizardContext): Promise<IHostJsonV2> {
const hostJson: IHostJsonV2 = await super.getHostContent(context);
let defaultExecutablePath: string = '';
const args: string[] = [];
// only set these for the users if they chose to include sample code that will match these parameters
if (context.includeSampleCode) {
switch (context.serverLanguage) {
case ProjectLanguage.Python:
defaultExecutablePath = 'python';
args.push('server.py');
break;
case ProjectLanguage.TypeScript:
defaultExecutablePath = 'npm';
args.push('run', 'start');
break;
case ProjectLanguage.CSharp:
defaultExecutablePath = 'dotnet';
args.push('server.dll');
break;
default:
break;
}
}
hostJson.customHandler = {
description: {
defaultExecutablePath,
workingDirectory: '',
arguments: args
}
};
hostJson.configurationProfile = "mcp-custom-handler";
return hostJson;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { AzureWizardPromptStep, type IAzureQuickPickItem } from "@microsoft/vscode-azext-utils";
import { type QuickPickOptions } from "vscode";
import { ProjectLanguage } from "../../../constants";
import { localize } from "../../../localize";
import { type MCPProjectWizardContext } from "../IProjectWizardContext";

export class MCPServerLanguagePromptStep extends AzureWizardPromptStep<MCPProjectWizardContext> {
public hideStepCount: boolean = true;

public constructor() {
super();
}

public shouldPrompt(wizardContext: MCPProjectWizardContext): boolean {
return !wizardContext.serverLanguage;
}

public async prompt(context: MCPProjectWizardContext): Promise<void> {
// Only display 'supported' languages that can be debugged in VS Code
const options: QuickPickOptions = { placeHolder: localize('selectServerLanguage', 'Select a language for the MCP server') };
const result = (await context.ui.showQuickPick(this.getPicks(), options)).data;
context.serverLanguage = result.language;
}

public getPicks(): IAzureQuickPickItem<{ language: ProjectLanguage }>[] {
return [
{ label: ProjectLanguage.Python, data: { language: ProjectLanguage.Python } },
{ label: ProjectLanguage.TypeScript, data: { language: ProjectLanguage.TypeScript } },
{ label: ProjectLanguage.CSharp, data: { language: ProjectLanguage.CSharp } },
];
}
}
Loading