Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
0922261
feat: add explicit model route readiness
Ankit6149 Jul 27, 2026
8611805
feat: require real model generation routes
Ankit6149 Jul 27, 2026
4955124
test: cover model route readiness
Ankit6149 Jul 27, 2026
f648e54
feat: use the studio workspace deliberately
Ankit6149 Jul 27, 2026
033b45d
feat: add SignalFlow MCP server package
Ankit6149 Jul 27, 2026
c39fc7c
feat: configure MCP provider secrets outside model context
Ankit6149 Jul 27, 2026
e62010e
feat: add MCP HTTP client for SignalFlow API
Ankit6149 Jul 27, 2026
a4d7ce2
feat: expose campaign creation through MCP tools
Ankit6149 Jul 27, 2026
22e546d
feat: implement dependency-free SignalFlow MCP stdio server
Ankit6149 Jul 27, 2026
8ef7d08
test: cover SignalFlow MCP tools
Ankit6149 Jul 27, 2026
a80ac74
test: smoke test MCP stdio protocol
Ankit6149 Jul 27, 2026
633e39d
docs: document SignalFlow MCP server setup
Ankit6149 Jul 27, 2026
d9ffd78
chore: add one-time studio and MCP product migration
Ankit6149 Jul 27, 2026
130bd17
chore: apply tested Studio and MCP product migration
Ankit6149 Jul 27, 2026
264e6b9
chore: separate product migration from workflow cleanup
Ankit6149 Jul 27, 2026
4a1ab33
feat: rebuild Studio model workflow and strict generation
github-actions[bot] Jul 27, 2026
add7751
ci: validate Studio, MCP, frontend, and backend together
Ankit6149 Jul 27, 2026
9d9c6d4
chore: remove completed one-time product migration
Ankit6149 Jul 27, 2026
b3b5c18
fix: align hosted provider readiness with owner access
Ankit6149 Jul 27, 2026
656a1cb
fix: keep hosted server keys owner-only
Ankit6149 Jul 27, 2026
ce117d8
fix: allow server model keys only for owner sessions
Ankit6149 Jul 27, 2026
0f41b25
fix: require reachable endpoints for hosted local models
Ankit6149 Jul 27, 2026
f389202
fix: report only model routes available to the current session
Ankit6149 Jul 27, 2026
b0fc21f
fix: test server model keys only inside owner sessions
Ankit6149 Jul 27, 2026
1331555
fix: do not advertise localhost models on hosted deployments
Ankit6149 Jul 27, 2026
2dba5a2
test: cover hosted provider and local endpoint boundaries
Ankit6149 Jul 27, 2026
38e2853
security: keep custom and local model endpoints owner-only
Ankit6149 Jul 27, 2026
1556c84
fix: enforce MCP lifecycle and protocol errors
Ankit6149 Jul 27, 2026
a204e88
test: cover MCP lifecycle and unknown tool errors
Ankit6149 Jul 27, 2026
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
12 changes: 12 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,18 @@ on:
branches: [main, master]

jobs:
mcp-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Node
uses: actions/setup-node@v4
with:
node-version: "22"
- name: Run MCP protocol and tool tests
working-directory: mcp
run: npm test

python-tests:
runs-on: ubuntu-latest
steps:
Expand Down
111 changes: 65 additions & 46 deletions frontend/app/api/launch_kit/route.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ import { ingestGitHubRepo } from "../../../lib/context/github";
import { ingestLocalRepo } from "../../../lib/context/localRepo";
import { fetchUrlContent } from "../../../lib/context/linkFetcher";
import { generateStudioPackage } from "../../../lib/ai/generateStudioPackage";
import { assertModelGenerationProvider } from "../../../lib/ai/generationPolicy.mjs";

const OWNER_ONLY_ENDPOINT_PROVIDERS = new Set(["custom", "ollama", "lmstudio"]);

export const maxDuration = 60;

Expand All @@ -20,24 +23,40 @@ export async function POST(request) {
const body = parsedBody && typeof parsedBody === "object" && !Array.isArray(parsedBody)
? parsedBody
: {};
const generator = normalizeTextInput(body.generator) || "template";
const requestedGenerator = normalizeTextInput(body.generator) || normalizeTextInput(process.env.DEFAULT_MODEL_PROVIDER);
let generator;
try {
generator = assertModelGenerationProvider(requestedGenerator);
} catch (error) {
return new Response(JSON.stringify({ ok: false, error: error.message }), {
status: 400,
headers: { "Content-Type": "application/json" },
});
}
const providerApiKey = normalizeTextInput(body.providerApiKey);

if (!isOwner && Boolean(process.env.SIGNALFLOW_ACCESS_KEY)) {
if (generator !== "template" && generator !== "offline" && !providerApiKey) {
return new Response(
JSON.stringify({
error: "This hosted workspace is private. Enter the owner's access key or supply your own personal API key in settings to use cloud providers.",
}),
{
status: 401,
headers: { "Content-Type": "application/json" },
}
);
}
if (!isOwner && OWNER_ONLY_ENDPOINT_PROVIDERS.has(generator)) {
return accessError || new Response(JSON.stringify({
ok: false,
error: "Custom and local model endpoints require an authenticated owner session.",
}), {
status: 401,
headers: { "Content-Type": "application/json" },
});
}

if (!isOwner && Boolean(process.env.SIGNALFLOW_ACCESS_KEY) && !providerApiKey) {
return new Response(
JSON.stringify({
error: "This hosted workspace is private. Enter the owner's access key or supply your own personal API key to use a cloud provider.",
}),
{
status: 401,
headers: { "Content-Type": "application/json" },
},
);
}

// 1. Validate inputs
const validation = validateGenerationInputs(body);
if (!validation.valid) {
return new Response(JSON.stringify({
Expand Down Expand Up @@ -71,14 +90,28 @@ export async function POST(request) {
const providerBaseUrl = normalizeTextInput(body.providerBaseUrl);
const documentText = normalizeDocumentText(body.document_text);

const publicHosted = process.env.SIGNALFLOW_PUBLIC_HOSTED === "true" || Boolean(process.env.VERCEL);
const configuredLocalBaseUrl = generator === "ollama"
? normalizeTextInput(process.env.OLLAMA_BASE_URL)
: generator === "lmstudio"
? normalizeTextInput(process.env.LMSTUDIO_BASE_URL)
: "";
if (["ollama", "lmstudio"].includes(generator) && publicHosted && !providerBaseUrl && !configuredLocalBaseUrl) {
return new Response(JSON.stringify({
ok: false,
error: "This hosted deployment needs a reachable model base URL. A browser-local localhost endpoint cannot be reached from the server.",
}), {
status: 400,
headers: { "Content-Type": "application/json" },
});
}

const warnings = [];
let repoContext = null;
const linksContext = [];
const mediaItems = Array.isArray(body.media_items) ? [...body.media_items] : [];

const githubToken = normalizeTextInput(body.github_token) || normalizeTextInput(body.githubToken);

// 2. Perform Repository Ingestion if repo URL or local path provided
if (repoUrl) {
try {
const isLocal = !repoUrl.includes("github.com") &&
Expand All @@ -89,45 +122,36 @@ export async function POST(request) {
repoUrl.startsWith(".") ||
(!repoUrl.includes("http://") && !repoUrl.includes("https://")));

if (isLocal) {
repoContext = await ingestLocalRepo(repoUrl);
} else {
repoContext = await ingestGitHubRepo(repoUrl, githubToken);
}
if (repoContext?.warnings?.length) {
warnings.push(...repoContext.warnings);
}
} catch (err) {
warnings.push(`Repository ingestion failed: ${err.message}. Generating with available inputs.`);
repoContext = isLocal
? await ingestLocalRepo(repoUrl)
: await ingestGitHubRepo(repoUrl, githubToken);

if (repoContext?.warnings?.length) warnings.push(...repoContext.warnings);
} catch (error) {
warnings.push(`Repository ingestion failed: ${error.message}. Generating with available inputs.`);
}
}

// 3. Perform Docs/Links scraping if urls provided
if (docsUrl) {
// Split by spaces or newlines to support multiple links
const urls = docsUrl.split(/\s+/).filter(Boolean);
for (const url of urls) {
try {
const fetchResult = await fetchUrlContent(url);
if (fetchResult) {
linksContext.push(fetchResult);
if (fetchResult.warnings?.length) {
warnings.push(...fetchResult.warnings);
}
if (fetchResult.warnings?.length) warnings.push(...fetchResult.warnings);
}
} catch (err) {
warnings.push(`Scraping docs link "${url}" failed: ${err.message}.`);
} catch (error) {
warnings.push(`Scraping docs link "${url}" failed: ${error.message}.`);
}
}
}

// 4. In V1, automated screenshot capture is disabled in the main flow.
if (appUrl) {
warnings.push("Automatic app capture is disabled in main flow. Upload screenshots or record manually.");
warnings.push("Automatic app capture is disabled in the main flow. Upload screenshots or record manually.");
}
void enableAutoCapture;

// 5. Build context & generate package (supports AI routes & templates fallbacks)
const result = await generateStudioPackage({
projectName,
notes,
Expand All @@ -146,25 +170,20 @@ export async function POST(request) {
apiKey: providerApiKey,
baseUrl: providerBaseUrl,
modelName: providerModelName,
allowServerKey: isOwner,
},
});

// Merge API warnings with generation warnings
const allWarnings = Array.from(new Set([...warnings, ...(result.warnings || [])]));

return new Response(JSON.stringify({
...result,
warnings: allWarnings,
}), {
return new Response(JSON.stringify({ ...result, warnings: allWarnings }), {
status: 200,
headers: { "Content-Type": "application/json" },
});

} catch (err) {
} catch (error) {
return new Response(JSON.stringify({
ok: false,
error: `Server failed to assemble kit: ${err.message}`,
warnings: [err.message],
error: `Server failed to assemble kit: ${error.message}`,
warnings: [error.message],
}), {
status: 500,
headers: { "Content-Type": "application/json" },
Expand Down
47 changes: 37 additions & 10 deletions frontend/app/api/provider_status/route.js
Original file line number Diff line number Diff line change
@@ -1,20 +1,47 @@
import { requireOwnerAccess } from "../_auth";
import { getProviderConfigurationStatus } from "../../../lib/ai/providerStatus";
import {
MODEL_GENERATION_PROVIDERS,
canUseServerProviderConfiguration,
} from "../../../lib/ai/generationPolicy.mjs";

const LOCAL_PROVIDERS = new Set(["ollama", "lmstudio"]);

export async function GET(request) {
try {
const status = getProviderConfigurationStatus();
const defaultProvider = process.env.DEFAULT_MODEL_PROVIDER || "";
return new Response(JSON.stringify({
providers: status,
defaultProvider: defaultProvider
}), {
const isOwner = requireOwnerAccess(request) === null;
const publicHosted = process.env.SIGNALFLOW_PUBLIC_HOSTED === "true" || Boolean(process.env.VERCEL);
const canUseServerConfiguration = canUseServerProviderConfiguration({
publicHosted,
allowServerKey: isOwner,
});

const allProviders = getProviderConfigurationStatus();
const providers = Object.fromEntries(
Object.entries(allProviders)
.filter(([id]) => MODEL_GENERATION_PROVIDERS.has(id))
.map(([id, provider]) => [id, {
...provider,
configured: Boolean(provider.configured && canUseServerConfiguration),
requiresBaseUrl: LOCAL_PROVIDERS.has(id) && publicHosted && !provider.configured,
}]),
);

const requestedDefault = String(process.env.DEFAULT_MODEL_PROVIDER || "").trim().toLowerCase();
const defaultProvider = MODEL_GENERATION_PROVIDERS.has(requestedDefault) ? requestedDefault : "";
const recommendedProvider =
(defaultProvider && providers[defaultProvider]?.configured ? defaultProvider : "") ||
Object.keys(providers).find((id) => providers[id]?.configured) ||
"gemini";

return new Response(JSON.stringify({ providers, defaultProvider, recommendedProvider }), {
status: 200,
headers: { "Content-Type": "application/json" }
headers: { "Content-Type": "application/json" },
});
} catch (err) {
return new Response(JSON.stringify({ error: err.message }), {
} catch (error) {
return new Response(JSON.stringify({ error: error.message }), {
status: 500,
headers: { "Content-Type": "application/json" }
headers: { "Content-Type": "application/json" },
});
}
}
Loading
Loading